@okostream/mcp 0.12.4 → 0.12.6

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 (3) hide show
  1. package/README.md +8 -4
  2. package/dist/server.js +115 -43
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -96,10 +96,14 @@ Refusals are explicit and never require guessing:
96
96
  OKO team.
97
97
  - `PLATFORM_REJECTED_READ` — no access to that resource, or it does not exist.
98
98
 
99
- Image URLs are resolved locally. On a machine where a VPN or proxy intercepts
100
- DNS, public hosts resolve to fake addresses (`198.18.x.x`); such uploads still
101
- work, and the result says so. Uploading a file from disk (`OKO_IMAGE_ROOTS`)
102
- always works.
99
+ Image URLs are fetched by the OKO server first, so a VPN, proxy or captive
100
+ portal on the organizer's machine is not in the way. If the server cannot
101
+ reach the URL (a private host, a login wall, a timeout), the MCP client
102
+ fetches it from the local machine; on a machine where a VPN or proxy
103
+ intercepts DNS, public hosts resolve to fake addresses (`198.18.x.x`), such
104
+ uploads still work, and the result says so. When both roads fail, the error
105
+ names both reasons and asks for a file from disk (`OKO_IMAGE_ROOTS`), which
106
+ always works. The result's `sourceVia` says who fetched the image.
103
107
 
104
108
  ## Capabilities
105
109
 
package/dist/server.js CHANGED
@@ -116223,7 +116223,7 @@ function sanitizeScopes(scopes) {
116223
116223
  }
116224
116224
 
116225
116225
  // build/version.js
116226
- var OKO_MCP_VERSION = "0.12.4";
116226
+ var OKO_MCP_VERSION = "0.12.6";
116227
116227
 
116228
116228
  // build/auth.js
116229
116229
  var state = null;
@@ -116465,6 +116465,15 @@ function isPublicUnicastAddress(address) {
116465
116465
  return false;
116466
116466
  return import_ipaddr.default.parse(normalized).range() === "unicast";
116467
116467
  }
116468
+ function pinnedLookup(address, family) {
116469
+ return (_hostname, options2, callback) => {
116470
+ if (options2 && typeof options2 === "object" && options2.all) {
116471
+ callback(null, [{ address, family }]);
116472
+ return;
116473
+ }
116474
+ callback(null, address, family);
116475
+ };
116476
+ }
116468
116477
 
116469
116478
  // build/tools/eventAccess.js
116470
116479
  var EVENT_ACCESS_FIELD_BY_MODE = {
@@ -116640,9 +116649,7 @@ async function readRemoteImage(rawUrl, redirects = 0) {
116640
116649
  remoteFetchNotes.push(`Image fetched through a VPN/proxy with DNS interception: ${url.hostname} resolved to ${pinned.address}, so OKO could not verify the destination address itself; the connection went through the proxy. This is expected on such machines and fine for public image links.`);
116641
116650
  }
116642
116651
  const dispatcher = new import_undici.Agent({
116643
- connect: {
116644
- lookup: (_hostname, _options, callback) => callback(null, pinned.address, pinned.family)
116645
- }
116652
+ connect: { lookup: pinnedLookup(pinned.address, pinned.family) }
116646
116653
  });
116647
116654
  const controller = new AbortController();
116648
116655
  const timeout = setTimeout(() => controller.abort(), REMOTE_IMAGE_TIMEOUT_MS);
@@ -116673,7 +116680,7 @@ async function readRemoteImage(rawUrl, redirects = 0) {
116673
116680
  if (Number.isFinite(contentLength) && contentLength > MAX_SOURCE_IMAGE_BYTES) {
116674
116681
  throw new Error("Image URL is larger than 15MB.");
116675
116682
  }
116676
- return responseBufferWithLimit(response, MAX_SOURCE_IMAGE_BYTES);
116683
+ return await responseBufferWithLimit(response, MAX_SOURCE_IMAGE_BYTES);
116677
116684
  } catch (err) {
116678
116685
  if (err instanceof Error && err.name === "AbortError") {
116679
116686
  throw new Error("Image URL fetch timed out.");
@@ -116710,12 +116717,65 @@ async function readLocalImage(inputPath) {
116710
116717
  throw new Error("Image file is larger than 15MB.");
116711
116718
  return readFile(filePath);
116712
116719
  }
116713
- async function readImageSource(source) {
116714
- if (source.path)
116715
- return readLocalImage(source.path);
116716
- if (source.url)
116717
- return readRemoteImage(source.url);
116718
- throw new Error("Provide image source path or url.");
116720
+ function serverFetchRefusal(error3) {
116721
+ if (!error3 || typeof error3 !== "object")
116722
+ return null;
116723
+ const details = error3.details;
116724
+ if (details && typeof details === "object" && details.code === "REMOTE_FETCH_FAILED") {
116725
+ const record2 = details;
116726
+ const fallbackMessage = String(error3.message ?? "the server could not fetch the URL");
116727
+ return {
116728
+ reason: typeof record2.reason === "string" ? record2.reason : "unknown",
116729
+ message: typeof record2.message === "string" ? record2.message : fallbackMessage
116730
+ };
116731
+ }
116732
+ const code = error3.code;
116733
+ const message = String(error3.message ?? "");
116734
+ if (code === "functions/invalid-argument" && message.includes("Image payload is empty or too large")) {
116735
+ return { reason: "server_without_url_support", message: "The OKO server release in use does not fetch URLs yet." };
116736
+ }
116737
+ return null;
116738
+ }
116739
+ var SERVER_FETCH_NOTE = "Image fetched by the OKO server directly from the URL; this machine's network was not involved.";
116740
+ function localFallbackNote(serverFailure) {
116741
+ return `The OKO server could not fetch the URL (${serverFailure.reason}: ${serverFailure.message}), so the image was fetched from this machine instead.`;
116742
+ }
116743
+ function remoteImageUnavailableMessage(url, serverFailure, localError) {
116744
+ const localMessage = localError instanceof Error ? localError.message : String(localError);
116745
+ return `Image URL could not be fetched by the OKO server or from this machine. Server (${serverFailure.reason}): ${serverFailure.message} This machine: ${localMessage} Ask the organizer to download the image and pass its path via source.path (allowed directories come from OKO_IMAGE_ROOTS), or to share a public HTTPS link that returns the image file directly. URL: ${url}`;
116746
+ }
116747
+ async function uploadImageFromSource(eventId, kind, cardId, source) {
116748
+ if (source.path) {
116749
+ const buffer2 = await readLocalImage(source.path);
116750
+ const uploaded2 = await uploadMasterImage(eventId, kind, cardId, { imageBase64: buffer2.toString("base64") });
116751
+ return { uploaded: uploaded2, sourceBytes: buffer2.byteLength, sourceVia: "client", notes: [] };
116752
+ }
116753
+ if (!source.url)
116754
+ throw new Error("Provide image source path or url.");
116755
+ let serverFailure;
116756
+ try {
116757
+ const uploaded2 = await uploadMasterImage(eventId, kind, cardId, { sourceUrl: source.url });
116758
+ return { uploaded: uploaded2, sourceBytes: uploaded2.sourceBytes ?? 0, sourceVia: "server", notes: [SERVER_FETCH_NOTE] };
116759
+ } catch (error3) {
116760
+ const refusal = serverFetchRefusal(error3);
116761
+ if (!refusal)
116762
+ throw error3;
116763
+ serverFailure = refusal;
116764
+ }
116765
+ let buffer;
116766
+ try {
116767
+ buffer = await readRemoteImage(source.url);
116768
+ } catch (localError) {
116769
+ takeRemoteFetchNotes();
116770
+ throw new Error(remoteImageUnavailableMessage(source.url, serverFailure, localError));
116771
+ }
116772
+ const uploaded = await uploadMasterImage(eventId, kind, cardId, { imageBase64: buffer.toString("base64") });
116773
+ return {
116774
+ uploaded,
116775
+ sourceBytes: buffer.byteLength,
116776
+ sourceVia: "client",
116777
+ notes: [localFallbackNote(serverFailure), ...takeRemoteFetchNotes()]
116778
+ };
116719
116779
  }
116720
116780
  function readImageRef(eventData, kind, cardId) {
116721
116781
  const blocks = eventData.blocks && typeof eventData.blocks === "object" ? eventData.blocks : {};
@@ -116853,8 +116913,8 @@ var cardItemInputSchema = external_exports.object({
116853
116913
  formId: cardIdSchema.nullable().optional()
116854
116914
  }).strict();
116855
116915
  var imageSourceSchema = external_exports.object({
116856
- path: external_exports.string().min(1).optional().describe("Local image path on the MCP client machine."),
116857
- url: external_exports.string().url().optional().describe("HTTPS image URL to fetch from the MCP client machine.")
116916
+ path: external_exports.string().min(1).optional().describe("Local image path on the MCP client machine (inside OKO_IMAGE_ROOTS)."),
116917
+ url: external_exports.string().url().optional().describe("Public HTTPS image URL. The OKO server fetches it first (works behind VPNs and proxies); if the server cannot reach it, the MCP client fetches it from this machine; if both fail, ask the organizer for a local file via path.")
116858
116918
  }).strict().refine((source) => Boolean(source.path) !== Boolean(source.url), {
116859
116919
  message: "Provide exactly one image source: path or url."
116860
116920
  });
@@ -116920,13 +116980,13 @@ async function deleteStorageUrls(eventId, masterUrl, fallbackUrl) {
116920
116980
  const warnings = await deleteStoragePaths(eventId, paths);
116921
116981
  return { attempted: [...new Set(paths)].length, warnings };
116922
116982
  }
116923
- async function uploadMasterImage(eventId, kind, cardId, image) {
116983
+ async function uploadMasterImage(eventId, kind, cardId, source) {
116924
116984
  const uploadImage = httpsCallable(functions, "uploadMcpImage");
116925
116985
  const result = await uploadImage({
116926
116986
  eventId,
116927
116987
  kind,
116928
116988
  ...cardId ? { cardId } : {},
116929
- imageBase64: image.toString("base64")
116989
+ ...source.sourceUrl ? { sourceUrl: source.sourceUrl } : { imageBase64: source.imageBase64 }
116930
116990
  });
116931
116991
  return result.data;
116932
116992
  }
@@ -117007,8 +117067,6 @@ async function uploadEventImageForUser(uid, eventId, kind, cardId, source) {
117007
117067
  throw new Error("cardId is only valid when kind is card.");
117008
117068
  const event = await readEvent(eventId, uid, "edit");
117009
117069
  const previous = readImageRef(event, kind, cardId);
117010
- const sourceBuffer = await readImageSource(source);
117011
- const remoteNotes = takeRemoteFetchNotes();
117012
117070
  const eventBlocks = event.blocks && typeof event.blocks === "object" ? event.blocks : {};
117013
117071
  const placeholderCfg = eventBlocks.video?.placeholder && typeof eventBlocks.video.placeholder === "object" ? eventBlocks.video.placeholder : {};
117014
117072
  const blockEnabled = kind === "promoBanner" ? eventBlocks.promoBanner?.enabled === true : kind === "videoPlaceholder" ? placeholderCfg.enabled === true : readCardsConfig(event).enabled;
@@ -117022,7 +117080,7 @@ async function uploadEventImageForUser(uid, eventId, kind, cardId, source) {
117022
117080
  const previousMasterPath = storagePathFromDownloadUrl(previous.imageMasterUrl);
117023
117081
  const deleteWarnings = [];
117024
117082
  let deletesAttempted = 0;
117025
- const uploaded = await uploadMasterImage(eventId, kind, cardId, sourceBuffer);
117083
+ const { uploaded, sourceBytes, sourceVia, notes: sourceNotes } = await uploadImageFromSource(eventId, kind, cardId, source);
117026
117084
  if (!uploaded)
117027
117085
  throw new Error("Image upload did not return a result.");
117028
117086
  if (kind !== "card" || previousMasterPath && previousMasterPath !== uploaded.storagePath || !previous.imageMasterUrl && previous.imageUrl) {
@@ -117037,14 +117095,15 @@ async function uploadEventImageForUser(uid, eventId, kind, cardId, source) {
117037
117095
  storagePath: uploaded.storagePath,
117038
117096
  masterUrl: uploaded.masterUrl,
117039
117097
  variantsPending: true,
117040
- sourceBytes: sourceBuffer.byteLength,
117098
+ sourceBytes,
117099
+ sourceVia,
117041
117100
  masterBytes: uploaded.masterBytes,
117042
117101
  imageWidth: uploaded.imageWidth,
117043
117102
  imageHeight: uploaded.imageHeight,
117044
117103
  note: "Image master uploaded. CF generateImageVariants should publish thumb/card/hero variants and update imageUrl/imageVariants in about 10-30 seconds.",
117045
117104
  storageDeletesAttempted: deletesAttempted,
117046
117105
  storageDeleteWarnings: deleteWarnings,
117047
- hints: [...remoteNotes, ...uploadHints]
117106
+ hints: [...sourceNotes, ...uploadHints]
117048
117107
  };
117049
117108
  }
117050
117109
 
@@ -117186,6 +117245,7 @@ var imageOutputSchema = external_exports.object({
117186
117245
  masterUrl: external_exports.string().url(),
117187
117246
  variantsPending: external_exports.boolean(),
117188
117247
  sourceBytes: external_exports.number().int().nonnegative(),
117248
+ sourceVia: external_exports.enum(["server", "client"]).optional(),
117189
117249
  masterBytes: external_exports.number().int().nonnegative(),
117190
117250
  imageWidth: external_exports.number().int().positive(),
117191
117251
  imageHeight: external_exports.number().int().positive(),
@@ -117363,7 +117423,7 @@ var SCOPE_UNLOCKS = {
117363
117423
  "chat:moderate": "chat mode, moderation, bans and mutes",
117364
117424
  "telegram:write": "claiming channels and publishing Telegram posts",
117365
117425
  "workspace:read": "listing workspaces, members, invites, notifications",
117366
- "workspace:write": "workspace management, invites, profile edits, moving events between workspaces",
117426
+ "workspace:write": "workspace management, invites, profile edits",
117367
117427
  "leads:read": "reading and exporting leads"
117368
117428
  };
117369
117429
  function presetsWithScope(scope) {
@@ -118033,6 +118093,25 @@ async function assertCanCreateInWorkspace(uid, workspaceId) {
118033
118093
  }
118034
118094
  }
118035
118095
  }
118096
+ function buildCreateWebinarDocument(input) {
118097
+ return {
118098
+ ownerId: input.uid,
118099
+ workspaceId: input.workspaceId,
118100
+ createdAt: input.createdAt,
118101
+ title: input.title.trim(),
118102
+ pinned: false,
118103
+ video: input.video,
118104
+ meta: {
118105
+ eventDate: input.eventDate,
118106
+ speakerName: input.speakerName,
118107
+ description: input.description
118108
+ },
118109
+ blocks: {
118110
+ chat: { enabled: input.chatEnabled, visibility: "always" },
118111
+ promoBanner: { enabled: false, text: "", link: "", visibility: "always" }
118112
+ }
118113
+ };
118114
+ }
118036
118115
  async function addEventWithRtdbMirror(eventData) {
118037
118116
  const eventRef = await addDoc(collection(firestore, "events"), eventData);
118038
118117
  try {
@@ -118317,23 +118396,17 @@ function registerEventTools(server2) {
118317
118396
  const workspaceId = args.workspaceId ?? uid;
118318
118397
  await assertCanCreateInWorkspace(uid, workspaceId);
118319
118398
  const video = normalizeVideo(args.videoUrl);
118320
- const docRef = await addEventWithRtdbMirror({
118321
- ownerId: uid,
118399
+ const docRef = await addEventWithRtdbMirror(buildCreateWebinarDocument({
118400
+ uid,
118322
118401
  workspaceId,
118323
- createdAt: serverTimestamp2(),
118324
- title: args.title.trim(),
118325
- pinned: false,
118402
+ title: args.title,
118326
118403
  video,
118327
- meta: {
118328
- eventDate: args.eventDate ? Timestamp.fromDate(new Date(args.eventDate)) : null,
118329
- speakerName: args.speakerName?.trim() || null,
118330
- description: args.description?.trim() || null
118331
- },
118332
- blocks: {
118333
- chat: { enabled: args.chatEnabled, visibility: "always" },
118334
- promoBanner: { enabled: false, text: "", link: "", visibility: "always" }
118335
- }
118336
- });
118404
+ eventDate: args.eventDate ? Timestamp.fromDate(new Date(args.eventDate)) : null,
118405
+ speakerName: args.speakerName?.trim() || null,
118406
+ description: args.description?.trim() || null,
118407
+ chatEnabled: args.chatEnabled,
118408
+ createdAt: serverTimestamp2()
118409
+ }));
118337
118410
  return textResult({
118338
118411
  id: docRef.id,
118339
118412
  ...eventLinks(docRef.id),
@@ -118643,10 +118716,10 @@ function registerImageTools(server2) {
118643
118716
  "eventId: event to edit.",
118644
118717
  "kind: promoBanner, videoPlaceholder, or card.",
118645
118718
  "cardId: required when kind=card; must match an existing blocks.cards.items id.",
118646
- "source: exactly one of local path or HTTPS image URL.",
118719
+ "source: exactly one of local path or public HTTPS image URL.",
118647
118720
  "confirm: must be true because the public event image may be replaced."
118648
118721
  ],
118649
- returns: "eventId, kind, imageId, Storage path, masterUrl, byte counts, and a note that CF-generated variants are pending.",
118722
+ returns: "eventId, kind, imageId, Storage path, masterUrl, byte counts, sourceVia (server or client fetched the URL), hints, and a note that CF-generated variants are pending.",
118650
118723
  examples: [
118651
118724
  'Use kind="promoBanner" with source.url to set a webinar banner from an image link.',
118652
118725
  'Use kind="card" with cardId after the card item already exists.'
@@ -118654,7 +118727,7 @@ function registerImageTools(server2) {
118654
118727
  notes: [
118655
118728
  "Uploading an image does not enable its block: toggle the block independently via oko_configure_blocks, in either order.",
118656
118729
  "The MCP server reads local paths on the organizer machine.",
118657
- "Remote URLs must be public HTTPS images, max 15MB; private/local hosts are blocked.",
118730
+ "A URL is fetched by the OKO server first (no VPN or proxy in the way), then from this machine if the server could not reach it; if both fail, the error names both reasons \u2014 ask the organizer to download the image and pass source.path. Public HTTPS only, max 15MB; private/local hosts are blocked.",
118658
118731
  "For card images, imageId is the cardId so OKO CF4 can update the correct item.",
118659
118732
  IMAGE_ADVICE.promoBanner,
118660
118733
  IMAGE_ADVICE.videoPlaceholder,
@@ -118891,7 +118964,6 @@ async function recordToolAudit(toolName, outcome, args, startedAt, error3) {
118891
118964
  var REPORT_URL = `${APP_ORIGIN}/contact`;
118892
118965
  var KEYS_URL = `${APP_ORIGIN}/app/mcp`;
118893
118966
  var SCOPE_WORKAROUNDS = {
118894
- oko_move_event: "Without workspace:write you can still place the event in another workspace: call oko_duplicate_event with targetWorkspaceId to create a copy there. The original stays where it is; live overrides (chat mode, placeholder visibility) are not copied and must be set again on the copy.",
118895
118967
  oko_trash_event: "This key cannot delete events. Tell the organizer which event to remove; they can do it in the OKO cabinet, or create a key with the full preset.",
118896
118968
  oko_delete_event_permanently: "This key cannot delete events. Tell the organizer which event to remove; they can do it in the OKO cabinet, or create a key with the full preset.",
118897
118969
  oko_publish_telegram_post: "This key cannot publish to Telegram. The organizer can publish the announcement from the OKO cabinet, or create a key with the full preset."
@@ -119205,7 +119277,7 @@ function registerLifecycleTools(server2) {
119205
119277
  notes: [
119206
119278
  "Copies title, video, meta, blocks, forms, and Telegram config; does not copy analytics sessions or leads.",
119207
119279
  "Live overrides are NOT copied: the copy starts with chat mode public and the placeholder hidden, whatever the source had. Set them again with oko_set_chat_mode and oko_set_block_visibility on the new event.",
119208
- "Requires only events:write plus createEvents in the target workspace, so it also works as the way to place an event in another workspace when the key lacks workspace:write for oko_move_event."
119280
+ "Requires only events:write plus createEvents in the target workspace. To move (not copy) an event, use oko_move_event with the same scope."
119209
119281
  ]
119210
119282
  }),
119211
119283
  inputSchema: {
@@ -119258,7 +119330,7 @@ function registerLifecycleTools(server2) {
119258
119330
  examples: ["Use when an event belongs in another workspace. Call oko_list_workspaces first to choose the target."],
119259
119331
  notes: [
119260
119332
  "Reads target workspace shares from Firestore with permissions; does not use RTDB workspaceMembers.",
119261
- "Requires the workspace:write scope (preset full). Keys with the operator preset cannot move events: use oko_duplicate_event with targetWorkspaceId instead, which creates a copy in the target workspace and leaves the original in place."
119333
+ "Requires events:write plus ownership of the event (or of its source workspace) and createEvents in the target workspace; no workspace:write needed. Live overrides (chat mode, placeholder) travel with the event: they live in the event's own RTDB node."
119262
119334
  ]
119263
119335
  }),
119264
119336
  inputSchema: {
@@ -119269,7 +119341,7 @@ function registerLifecycleTools(server2) {
119269
119341
  outputSchema: updateOutputSchema,
119270
119342
  annotations: writeTool
119271
119343
  }, async ({ eventId, targetWorkspaceId, confirm }) => {
119272
- await authed(["events:write", "workspace:write"]);
119344
+ await authed(["events:write"]);
119273
119345
  requireConfirm(confirm, "oko_move_event", { eventId, targetWorkspaceId });
119274
119346
  const moveEvent = httpsCallable(functions, "moveEventToWorkspace");
119275
119347
  const result = await moveEvent({ eventId, targetWorkspaceId });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okostream/mcp",
3
- "version": "0.12.4",
3
+ "version": "0.12.6",
4
4
  "description": "Control OKO Stream webinars from Claude, Codex, and other MCP clients.",
5
5
  "license": "UNLICENSED",
6
6
  "author": {