@okostream/mcp 0.13.1 → 0.14.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.
Files changed (2) hide show
  1. package/dist/server.js +47 -12
  2. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -116230,6 +116230,15 @@ if (!env.firebase.chatDatabaseURL) {
116230
116230
  throw new Error("OKO MCP: chat database URL is not configured. Set OKO_FIREBASE_CHAT_DB_URL (or keep the bundled default) \u2014 chat paths do not exist in the main database.");
116231
116231
  }
116232
116232
  var chatRtdb = getDatabase(firebaseApp, env.firebase.chatDatabaseURL);
116233
+ async function serverNowMs(db = chatRtdb) {
116234
+ const offset = await new Promise((resolve3) => {
116235
+ onValue(ref(db, ".info/serverTimeOffset"), (snap) => {
116236
+ const v = snap.val();
116237
+ resolve3(typeof v === "number" && Number.isFinite(v) ? v : 0);
116238
+ }, () => resolve3(0), { onlyOnce: true });
116239
+ });
116240
+ return Date.now() + offset;
116241
+ }
116233
116242
  var functions = getFunctions(firebaseApp, "us-central1");
116234
116243
 
116235
116244
  // build/scopes.js
@@ -116291,7 +116300,7 @@ function sanitizeScopes(scopes) {
116291
116300
  }
116292
116301
 
116293
116302
  // build/version.js
116294
- var OKO_MCP_VERSION = "0.13.1";
116303
+ var OKO_MCP_VERSION = "0.14.0";
116295
116304
 
116296
116305
  // build/auth.js
116297
116306
  var state = null;
@@ -117601,13 +117610,24 @@ var notificationsMarkOutputSchema = external_exports.object({
117601
117610
  markedRead: external_exports.number().int().nonnegative(),
117602
117611
  notificationIds: external_exports.array(external_exports.string())
117603
117612
  }).passthrough();
117613
+ var FORM_FIELD_TYPES = ["text", "tel", "email", "textarea", "select", "checkbox"];
117604
117614
  var formFieldSchema = external_exports.object({
117605
117615
  id: external_exports.string().min(1).optional(),
117606
117616
  label: external_exports.string().max(120),
117607
- type: external_exports.enum(["text", "tel", "email", "textarea"]).default("text"),
117617
+ type: external_exports.enum(FORM_FIELD_TYPES).default("text").describe("text | tel | email | textarea answer with free text; select (one answer) and checkbox (several) answer with one of `options`."),
117608
117618
  required: external_exports.boolean().default(false),
117609
- placeholder: external_exports.string().max(160).nullable().optional()
117610
- }).strict();
117619
+ placeholder: external_exports.string().max(160).nullable().optional(),
117620
+ options: external_exports.array(external_exports.string().trim().min(1).max(80)).max(20).optional().describe("Choices of a select or checkbox field: 1\u201320 unique option texts. The lead stores the option text itself \u2014 a string for select, an array of strings for checkbox.")
117621
+ }).strict().superRefine((field2, ctx) => {
117622
+ const choice = field2.type === "select" || field2.type === "checkbox";
117623
+ const count = new Set(field2.options ?? []).size;
117624
+ if (choice && count === 0) {
117625
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["options"], message: `A ${field2.type} field needs at least one option.` });
117626
+ }
117627
+ if (!choice && field2.options !== void 0) {
117628
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["options"], message: `options belong to select or checkbox fields only; this field is ${field2.type}.` });
117629
+ }
117630
+ });
117611
117631
  var formSchema = external_exports.object({
117612
117632
  id: external_exports.string().min(1).optional(),
117613
117633
  title: external_exports.string().max(160).nullable().optional(),
@@ -118346,7 +118366,7 @@ function registerChatTools(server2) {
118346
118366
  const { uid } = await authed(["chat:moderate"]);
118347
118367
  await readEvent(eventId, uid, "moderate");
118348
118368
  requireConfirm(confirm, "oko_delete_chat_message", { eventId, messageId });
118349
- await set(ref(chatRtdb, `mod/deleted/${eventId}/${messageId}`), { deletedAt: serverTimestamp(), ts: Date.now(), by: uid, reason: "manual" });
118369
+ await set(ref(chatRtdb, `mod/deleted/${eventId}/${messageId}`), { deletedAt: serverTimestamp(), ts: await serverNowMs(), by: uid, reason: "manual" });
118350
118370
  return textResult({ eventId, messageId, deleted: true });
118351
118371
  });
118352
118372
  server2.registerTool("oko_mute_user", {
@@ -118364,7 +118384,7 @@ function registerChatTools(server2) {
118364
118384
  const actor = await authed(["chat:moderate"]);
118365
118385
  await readEvent(eventId, actor.uid, "moderate");
118366
118386
  requireConfirm(confirm, "oko_mute_user", { eventId, targetUid, minutes });
118367
- await set(ref(chatRtdb, `mutes/${eventId}/${targetUid}`), { until: Date.now() + minutes * 6e4, by: actor.uid, reason: "manual" });
118387
+ await set(ref(chatRtdb, `mutes/${eventId}/${targetUid}`), { until: await serverNowMs() + minutes * 6e4, by: actor.uid, reason: "manual" });
118368
118388
  return textResult({ eventId, targetUid, mutedMinutes: minutes });
118369
118389
  });
118370
118390
  server2.registerTool("oko_ban_user", {
@@ -118386,9 +118406,10 @@ function registerChatTools(server2) {
118386
118406
  const messagesSnap = await get(query(ref(chatRtdb, `messages/${eventId}`), limitToLast(500)));
118387
118407
  const messages = messagesSnap.val() ?? {};
118388
118408
  const deletes = {};
118409
+ const nowMs = await serverNowMs();
118389
118410
  for (const [messageId, message] of Object.entries(messages)) {
118390
118411
  if (message.uid === targetUid)
118391
- deletes[`mod/deleted/${eventId}/${messageId}`] = { deletedAt: serverTimestamp(), ts: Date.now(), by: actor.uid, reason: "ban" };
118412
+ deletes[`mod/deleted/${eventId}/${messageId}`] = { deletedAt: serverTimestamp(), ts: nowMs, by: actor.uid, reason: "ban" };
118392
118413
  }
118393
118414
  if (Object.keys(deletes).length > 0)
118394
118415
  await update(ref(chatRtdb), deletes);
@@ -118543,7 +118564,7 @@ function eventRuntimeHint(runtime) {
118543
118564
  }
118544
118565
 
118545
118566
  // build/tools/setupValidation.js
118546
- function validateEventSetup(eventId, event) {
118567
+ function validateEventSetup(eventId, event, nowMs = Date.now()) {
118547
118568
  const errors = [];
118548
118569
  const warnings = [];
118549
118570
  const suggestions = [];
@@ -118564,7 +118585,7 @@ function validateEventSetup(eventId, event) {
118564
118585
  const eventDateMs = toMillis(meta2.eventDate);
118565
118586
  if (eventDateMs == null) {
118566
118587
  add(suggestions, "date_missing", "meta.eventDate", "Add the webinar date/time if it is known.");
118567
- } else if (eventDateMs < Date.now() - 60 * 60 * 1e3) {
118588
+ } else if (eventDateMs < nowMs - 60 * 60 * 1e3) {
118568
118589
  add(warnings, "date_in_past", "meta.eventDate", "Configured webinar date appears to be in the past.");
118569
118590
  }
118570
118591
  if (scheduleMissesDate(event.analyticsSessionMode, meta2.eventDate)) {
@@ -118583,6 +118604,16 @@ function validateEventSetup(eventId, event) {
118583
118604
  const fields = Array.isArray(form.fields) ? form.fields : [];
118584
118605
  if (fields.length === 0)
118585
118606
  add(warnings, "form_fields_missing", `forms[${index}].fields`, "Lead form has no fields.");
118607
+ fields.forEach((rawField, fieldIndex) => {
118608
+ const field2 = asRecord(rawField);
118609
+ const type = safeString(field2.type);
118610
+ if (type !== "select" && type !== "checkbox")
118611
+ return;
118612
+ const options2 = Array.isArray(field2.options) ? field2.options : [];
118613
+ if (!options2.some((item) => typeof item === "string" && item.trim())) {
118614
+ add(errors, "form_field_options_missing", `forms[${index}].fields[${fieldIndex}].options`, `A ${type} field has no options, so the viewer sees an empty list. Add at least one option or change the field type.`);
118615
+ }
118616
+ });
118586
118617
  });
118587
118618
  for (const id of duplicateFormIds)
118588
118619
  add(errors, "form_id_duplicate", "forms", `Duplicate form id: ${id}.`);
@@ -118853,7 +118884,7 @@ function registerEventTools(server2) {
118853
118884
  }, async ({ eventId }) => {
118854
118885
  const { uid } = await authed(["events:read"]);
118855
118886
  const data = await readEvent(eventId, uid, "read");
118856
- return textResult(validateEventSetup(eventId, data));
118887
+ return textResult(validateEventSetup(eventId, data, await serverNowMs()));
118857
118888
  });
118858
118889
  server2.registerTool("oko_create_webinar", {
118859
118890
  title: "Create webinar",
@@ -119083,7 +119114,11 @@ function registerEventTools(server2) {
119083
119114
  label: field2.label.trim(),
119084
119115
  type: field2.type,
119085
119116
  required: field2.required === true,
119086
- placeholder: field2.placeholder?.trim() || null
119117
+ placeholder: field2.placeholder?.trim() || null,
119118
+ // Options are written for choice types only (the schema refuses them
119119
+ // elsewhere); duplicates collapse, order is kept — the viewer sees the
119120
+ // list in this order and a checkbox answer is stored in it.
119121
+ ...field2.type === "select" || field2.type === "checkbox" ? { options: [...new Set((field2.options ?? []).map((option) => option.trim()).filter(Boolean))] } : {}
119087
119122
  }))
119088
119123
  }));
119089
119124
  Object.assign(patch, telegramPatch);
@@ -119654,7 +119689,7 @@ function registerLeadTools(server2) {
119654
119689
  "limit: max rows, default 50, max 200.",
119655
119690
  "cursor: lead document id returned as nextCursor by the previous page."
119656
119691
  ],
119657
- returns: "leads or csv plus count, limit, hasMore/has_more, nextCursor/next_cursor. Without formId the rows are newest-first across pages; with formId they are newest-first WITHIN a page only, because paging there follows document ids \u2014 to rank one form's submissions by time, read every page and sort by submittedAt.",
119692
+ returns: "leads or csv plus count, limit, hasMore/has_more, nextCursor/next_cursor. A lead's values are keyed by field id: a checkbox field answers with an array of option texts, every other field with a string (in the CSV both sit inside valuesJson). Without formId the rows are newest-first across pages; with formId they are newest-first WITHIN a page only, because paging there follows document ids \u2014 to rank one form's submissions by time, read every page and sort by submittedAt.",
119658
119693
  examples: ["Use formId to export one form; if Firestore requests an index, create it from the error link and rerun."]
119659
119694
  }),
119660
119695
  inputSchema: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okostream/mcp",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "Control OKO Stream webinars from Claude, Codex, and other MCP clients.",
5
5
  "license": "UNLICENSED",
6
6
  "author": {