@okostream/mcp 0.12.9 → 0.12.11

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 +125 -37
  2. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -70499,6 +70499,13 @@ var OKO_CONNECTION_STATUSES = [
70499
70499
  "connected",
70500
70500
  "not_configured",
70501
70501
  "invalid_key",
70502
+ // 🔴 Next to `invalid_key`, not instead of it (A7, `LR3a-2`). The key is
70503
+ // valid and the account behind it is switched off or blocked — the one state
70504
+ // in which "create a fresh key" is not merely unhelpful but impossible, and
70505
+ // the state in which the platform used to give exactly that advice.
70506
+ // A genuinely revoked key still needs the create-a-key step, so the two
70507
+ // states are separate codes with separate steps.
70508
+ "account_inactive",
70502
70509
  "update_required",
70503
70510
  "rate_limited",
70504
70511
  "unavailable"
@@ -70539,6 +70546,18 @@ function nextSteps(status, minimumVersion, latestVersion) {
70539
70546
  label: "Restart or reconnect the MCP server after updating the key."
70540
70547
  }
70541
70548
  ];
70549
+ case "account_inactive":
70550
+ return [
70551
+ {
70552
+ id: "contact_support",
70553
+ label: "Ask OKO support to restore organizer access for this account: the key is valid, the account is switched off or blocked. Creating another key will not help \u2014 key creation needs an active account too.",
70554
+ url: OKO_ONBOARDING_URLS.website
70555
+ },
70556
+ {
70557
+ id: "restart_client",
70558
+ label: "Restart or reconnect the MCP server once access is restored."
70559
+ }
70560
+ ];
70542
70561
  case "update_required":
70543
70562
  return [
70544
70563
  {
@@ -70566,7 +70585,7 @@ function nextSteps(status, minimumVersion, latestVersion) {
70566
70585
  function buildConnectionStatus(status, options2 = {}) {
70567
70586
  const connected = status === "connected";
70568
70587
  const errorCode = connected ? null : status === "update_required" ? "CLIENT_UPGRADE_REQUIRED" : status === "rate_limited" ? "RATE_LIMITED" : status === "unavailable" ? "SERVICE_UNAVAILABLE" : "AUTH_REQUIRED";
70569
- const message = connected ? "OKO MCP is connected to an active organizer account." : status === "not_configured" ? "OKO MCP is installed, but OKO_API_KEY is not configured." : status === "invalid_key" ? "OKO MCP is installed, but the configured API key is invalid or revoked." : status === "update_required" ? options2.serverMessage ?? "This OKO MCP client version is no longer supported." : status === "rate_limited" ? "OKO temporarily rate-limited the connection attempt." : "OKO MCP could not verify the organizer connection.";
70588
+ const message = connected ? "OKO MCP is connected to an active organizer account." : status === "not_configured" ? "OKO MCP is installed, but OKO_API_KEY is not configured." : status === "invalid_key" ? "OKO MCP is installed, but the configured API key is invalid or revoked." : status === "account_inactive" ? options2.serverMessage ?? "The OKO API key is valid, but the organizer account behind it is switched off or blocked." : status === "update_required" ? options2.serverMessage ?? "This OKO MCP client version is no longer supported." : status === "rate_limited" ? "OKO temporarily rate-limited the connection attempt." : "OKO MCP could not verify the organizer connection.";
70570
70589
  return {
70571
70590
  connected,
70572
70591
  status,
@@ -116272,7 +116291,7 @@ function sanitizeScopes(scopes) {
116272
116291
  }
116273
116292
 
116274
116293
  // build/version.js
116275
- var OKO_MCP_VERSION = "0.12.9";
116294
+ var OKO_MCP_VERSION = "0.12.11";
116276
116295
 
116277
116296
  // build/auth.js
116278
116297
  var state = null;
@@ -116345,6 +116364,17 @@ var OkoAuthError = class extends Error {
116345
116364
  function versionField(value) {
116346
116365
  return typeof value === "string" && parseVersion(value) ? value.trim() : void 0;
116347
116366
  }
116367
+ function inactiveOrganizerError(body) {
116368
+ const parsed = body && typeof body === "object" ? body : {};
116369
+ if (parsed.error !== "organizer_inactive")
116370
+ return null;
116371
+ const serverMessage = typeof parsed.message === "string" && parsed.message.trim() ? parsed.message.trim() : void 0;
116372
+ return new OkoAuthError("account_inactive", serverMessage ?? "The OKO API key is valid, but the organizer account behind it is switched off or blocked. Creating another key will not help \u2014 contact OKO support to restore access.", { serverMessage });
116373
+ }
116374
+ function serverRefusalNote(body) {
116375
+ const parsed = body && typeof body === "object" ? body : {};
116376
+ return typeof parsed.message === "string" && parsed.message.trim() ? parsed.message.trim() : void 0;
116377
+ }
116348
116378
  function upgradeRequiredError(body) {
116349
116379
  const parsed = body && typeof body === "object" ? body : {};
116350
116380
  const minimumVersion = versionField(parsed.minimumVersion);
@@ -116379,7 +116409,16 @@ async function exchangeApiKey() {
116379
116409
  if (status === 429) {
116380
116410
  throw new OkoAuthError("rate_limited", "OKO API key exchange is temporarily rate-limited. Wait one minute, then restart the MCP client.");
116381
116411
  }
116382
- 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", `OKO organizer authentication failed with HTTP ${status}. Check your connection and try again.`);
116412
+ if (status === 401) {
116413
+ const inactive = inactiveOrganizerError(errorBody);
116414
+ if (inactive)
116415
+ throw inactive;
116416
+ }
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
+ `OKO organizer authentication failed with HTTP ${status}.`,
116419
+ serverRefusalNote(errorBody),
116420
+ "Check your connection and try again."
116421
+ ].filter(Boolean).join(" "));
116383
116422
  }
116384
116423
  const data = await response.json();
116385
116424
  if (typeof data.customToken !== "string") {
@@ -116611,17 +116650,19 @@ async function readEventAccess(eventId, uid, mode) {
116611
116650
  return null;
116612
116651
  })
116613
116652
  ]);
116614
- if (!snap.exists())
116615
- throw new Error("Event not found.");
116616
- const data = snap.data();
116617
- const allowed = grant === null ? legacyEventAllowsMode(data, uid, mode) : data.ownerId === uid || grant.exists() && grantAllowsMode(grant.data(), mode);
116618
- return { data, allowed };
116653
+ const data = snap.data() ?? {};
116654
+ const holds = (wanted) => grant === null ? legacyEventAllowsMode(data, uid, wanted) : data.ownerId === uid || grant.exists() && grantAllowsMode(grant.data(), wanted);
116655
+ return { data, exists: snap.exists(), visible: holds("read"), allowed: holds(mode) };
116619
116656
  }
116620
116657
  async function eventGrantsMode(eventId, uid, mode) {
116621
116658
  return (await readEventAccess(eventId, uid, mode)).allowed;
116622
116659
  }
116660
+ var EVENT_NOT_FOUND_MESSAGE = "Event not found.";
116623
116661
  async function readEvent(eventId, uid, mode = "read") {
116624
- const { data, allowed } = await readEventAccess(eventId, uid, mode);
116662
+ const { data, exists, visible, allowed } = await readEventAccess(eventId, uid, mode);
116663
+ if (!visible || !exists) {
116664
+ throw new Error(EVENT_NOT_FOUND_MESSAGE);
116665
+ }
116625
116666
  if (!allowed) {
116626
116667
  throw new Error(`Event does not grant ${mode} access to this organizer.`);
116627
116668
  }
@@ -117154,16 +117195,43 @@ async function updateCardImageInTransaction(eventId, cardId, patch) {
117154
117195
  });
117155
117196
  });
117156
117197
  }
117157
- async function writeEventPatchWithCards(eventId, basePatch, buildItems) {
117198
+ function descendPath(root, segments) {
117199
+ let current = root;
117200
+ for (const segment of segments) {
117201
+ if (current === null || typeof current !== "object")
117202
+ return void 0;
117203
+ current = current[segment];
117204
+ }
117205
+ return current;
117206
+ }
117207
+ function valueAfterPatch(fresh, patch, path) {
117208
+ if (Object.prototype.hasOwnProperty.call(patch, path))
117209
+ return patch[path];
117210
+ const segments = path.split(".");
117211
+ for (let cut = segments.length - 1; cut > 0; cut--) {
117212
+ const ancestor = segments.slice(0, cut).join(".");
117213
+ if (!Object.prototype.hasOwnProperty.call(patch, ancestor))
117214
+ continue;
117215
+ return descendPath(patch[ancestor], segments.slice(cut));
117216
+ }
117217
+ return descendPath(fresh, segments);
117218
+ }
117219
+ function scheduleMissesDate(sessionMode, eventDate) {
117220
+ return sessionMode === "schedule" && !eventDate;
117221
+ }
117222
+ async function writeEventPatchWithCards(eventId, basePatch, buildItems, validate = null) {
117158
117223
  const eventRef = doc(firestore, "events", eventId);
117159
117224
  await runTransaction2(firestore, async (transaction) => {
117160
117225
  const snap = await transaction.get(eventRef);
117161
117226
  if (!snap.exists())
117162
117227
  throw new Error("Event not found.");
117228
+ const fresh = snap.data();
117163
117229
  const patch = { ...basePatch };
117164
117230
  if (buildItems) {
117165
- patch["blocks.cards.items"] = buildItems(snap.data());
117231
+ patch["blocks.cards.items"] = buildItems(fresh);
117166
117232
  }
117233
+ if (validate)
117234
+ validate(fresh, patch);
117167
117235
  if (Object.keys(patch).length === 0)
117168
117236
  return;
117169
117237
  transaction.update(eventRef, patch);
@@ -118384,6 +118452,9 @@ function validateEventSetup(eventId, event) {
118384
118452
  } else if (eventDateMs < Date.now() - 60 * 60 * 1e3) {
118385
118453
  add(warnings, "date_in_past", "meta.eventDate", "Configured webinar date appears to be in the past.");
118386
118454
  }
118455
+ if (scheduleMissesDate(event.analyticsSessionMode, meta2.eventDate)) {
118456
+ 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.");
118457
+ }
118387
118458
  const duplicateFormIds = /* @__PURE__ */ new Set();
118388
118459
  const seenFormIds = /* @__PURE__ */ new Set();
118389
118460
  forms.forEach((form, index) => {
@@ -118499,6 +118570,24 @@ function telegramBlocksPatch(args) {
118499
118570
  "telegram.channelIds": args.telegramChannelIds
118500
118571
  };
118501
118572
  }
118573
+ function assertScheduleHasDate(fresh, patch) {
118574
+ const missing = scheduleMissesDate(valueAfterPatch(fresh, patch, "analyticsSessionMode"), valueAfterPatch(fresh, patch, "meta.eventDate"));
118575
+ if (!missing)
118576
+ return;
118577
+ 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
+ }
118579
+ 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"]) {
118583
+ if (!Object.prototype.hasOwnProperty.call(patch, field2))
118584
+ continue;
118585
+ const value = patch[field2];
118586
+ if (typeof value === "string" && value && !knownFormIds.has(value)) {
118587
+ 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
+ }
118589
+ }
118590
+ }
118502
118591
  function configureBlocksScopes(args) {
118503
118592
  return Object.keys(telegramBlocksPatch(args)).length > 0 ? ["setup:write", "telegram:write"] : ["setup:write"];
118504
118593
  }
@@ -118678,7 +118767,8 @@ function registerEventTools(server2) {
118678
118767
  returns: "event id and the list of Firestore fields updated.",
118679
118768
  examples: ["Use for title/date/speaker/video edits; use oko_configure_blocks for blocks and forms."],
118680
118769
  notes: [
118681
- "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."
118770
+ "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.",
118771
+ "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."
118682
118772
  ]
118683
118773
  }),
118684
118774
  inputSchema: {
@@ -118704,13 +118794,7 @@ function registerEventTools(server2) {
118704
118794
  if (requiredScopes.size === 0)
118705
118795
  throw new Error("Provide at least one field to update.");
118706
118796
  const { uid } = await authed([...requiredScopes]);
118707
- const current = await readEvent(args.eventId, uid, "edit");
118708
- if (args.sessionMode === "schedule") {
118709
- const dateAfterPatch = args.eventDate !== void 0 ? args.eventDate : current.meta?.eventDate ?? null;
118710
- if (!dateAfterPatch) {
118711
- 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'.");
118712
- }
118713
- }
118797
+ await readEvent(args.eventId, uid, "edit");
118714
118798
  const patch = {};
118715
118799
  if (args.title !== void 0)
118716
118800
  patch.title = args.title.trim();
@@ -118735,7 +118819,7 @@ function registerEventTools(server2) {
118735
118819
  patch.analyticsSessionMode = args.sessionMode;
118736
118820
  if (args.durationMinutes !== void 0)
118737
118821
  patch["meta.durationMinutes"] = args.durationMinutes;
118738
- await updateDoc(doc(firestore, "events", args.eventId), patch);
118822
+ await writeEventPatchWithCards(args.eventId, patch, null, assertScheduleHasDate);
118739
118823
  return textResult({ id: args.eventId, updated: Object.keys(patch) });
118740
118824
  });
118741
118825
  server2.registerTool("oko_configure_blocks", {
@@ -118833,17 +118917,7 @@ function registerEventTools(server2) {
118833
118917
  }))
118834
118918
  }));
118835
118919
  Object.assign(patch, telegramPatch);
118836
- const formsAfterPatch = patch.forms ?? current.forms;
118837
- const knownFormIds = new Set((Array.isArray(formsAfterPatch) ? formsAfterPatch : []).map((form) => typeof form?.id === "string" ? form.id : null).filter((id) => Boolean(id)));
118838
- for (const [field2, value] of [
118839
- ["blocks.promoBanner.formId", patch["blocks.promoBanner.formId"]],
118840
- ["blocks.primaryCta.formId", patch["blocks.primaryCta.formId"]]
118841
- ]) {
118842
- if (typeof value === "string" && value && !knownFormIds.has(value)) {
118843
- 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.`);
118844
- }
118845
- }
118846
- await updateDoc(doc(firestore, "events", args.eventId), patch);
118920
+ await writeEventPatchWithCards(args.eventId, patch, null, assertFormBindingsExist);
118847
118921
  const hints = [];
118848
118922
  if (args.chat?.visibility !== void 0) {
118849
118923
  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.");
@@ -119676,14 +119750,18 @@ function registerLifecycleTools(server2) {
119676
119750
  }
119677
119751
 
119678
119752
  // build/tools/live.js
119753
+ var CHAT_LIVE_SETTINGS = ["mode", "slowModeMs", "automodEnabled", "allowViewerLinks"];
119679
119754
  function registerLiveTools(server2) {
119680
119755
  server2.registerTool("oko_get_live_state", {
119681
119756
  title: "Get live state",
119682
119757
  description: toolDescription({
119683
119758
  summary: "Read main RTDB live controls, aggregate analytics, and chat settings for an event.",
119684
119759
  args: ["eventId: event to inspect."],
119685
- returns: "eventId, agg metrics, ctrl live overrides, and chat settings.",
119686
- examples: ["Use during a webinar before changing live block visibility or starting/ending sessions."]
119760
+ returns: "eventId, agg metrics, ctrl live overrides, and chat settings (mode, slowModeMs, automodEnabled, allowViewerLinks).",
119761
+ examples: ["Use during a webinar before changing live block visibility or starting/ending sessions."],
119762
+ notes: [
119763
+ "Live state does not carry the event team: to see who has access to an event and with which permissions, use oko_list_workspace_access on the event's workspace."
119764
+ ]
119687
119765
  }),
119688
119766
  inputSchema: { eventId: eventIdField },
119689
119767
  outputSchema: liveStateOutputSchema,
@@ -119691,12 +119769,18 @@ function registerLiveTools(server2) {
119691
119769
  }, async ({ eventId }) => {
119692
119770
  const { uid } = await authed(["analytics:read"]);
119693
119771
  await readEvent(eventId, uid, "analytics");
119694
- const [aggSnap, ctrlSnap, chatSettingsSnap] = await Promise.all([
119772
+ const [aggSnap, ctrlSnap, ...chatSnaps] = await Promise.all([
119695
119773
  get(ref(rtdb, `events/${eventId}/agg`)),
119696
119774
  get(ref(rtdb, `events/${eventId}/ctrl`)),
119697
- get(ref(chatRtdb, `settings/${eventId}`))
119775
+ ...CHAT_LIVE_SETTINGS.map((field2) => get(ref(chatRtdb, `settings/${eventId}/${field2}`)))
119698
119776
  ]);
119699
- return textResult({ eventId, agg: aggSnap.val() ?? {}, ctrl: ctrlSnap.val() ?? {}, chat: chatSettingsSnap.val() ?? {} });
119777
+ const chat = {};
119778
+ CHAT_LIVE_SETTINGS.forEach((field2, index) => {
119779
+ const value = chatSnaps[index]?.val();
119780
+ if (value !== null && value !== void 0)
119781
+ chat[field2] = value;
119782
+ });
119783
+ return textResult({ eventId, agg: aggSnap.val() ?? {}, ctrl: ctrlSnap.val() ?? {}, chat });
119700
119784
  });
119701
119785
  server2.registerTool("oko_set_block_visibility", {
119702
119786
  title: "Set live block visibility",
@@ -120831,7 +120915,11 @@ function registerWorkspaceTools(server2) {
120831
120915
  "workspaceId: workspace owned by the authenticated organizer.",
120832
120916
  "code: optional invite code, 16-64 chars; omitted means generated (recommended).",
120833
120917
  "email: optional intended recipient email.",
120834
- "permissions: optional whitelist of workspace permission booleans.",
120918
+ // The schema is `workspacePermissionsSchema`: nine booleans, all
120919
+ // required, no extra keys. "optional whitelist" read as "send the
120920
+ // ones you want", and every such call was rejected by validation
120921
+ // before it reached the platform.
120922
+ "permissions: required object of all nine workspace permission booleans (createEvents, editSetup, deleteEvents, controlLive, moderateChat, startSession, viewAnalytics, downloadReports, sendNotifications) \u2014 every key must be present, extra keys are rejected; set false for the ones the invitee should not get.",
120835
120923
  "confirm: must be true."
120836
120924
  ],
120837
120925
  returns: "workspaceId, invite code, inviteUrl, and created=true.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okostream/mcp",
3
- "version": "0.12.9",
3
+ "version": "0.12.11",
4
4
  "description": "Control OKO Stream webinars from Claude, Codex, and other MCP clients.",
5
5
  "license": "UNLICENSED",
6
6
  "author": {