@okostream/mcp 0.5.1 → 0.5.3

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.3 - 2026-07-10
4
+
5
+ - Added an explicit product model: OKO wraps an organizer-supplied video player and does not host or start the underlying stream.
6
+ - Added `oko://guide/nmo` and `oko://guide/telegram` for the complete NMO/CME and Telegram claim/publication workflows.
7
+ - Documented form-button precedence, MCP product boundaries, and the Telegram auto-reminder limitation.
8
+ - Updated the README and companion skill, and added protocol coverage for all seven onboarding resources.
9
+
10
+ ## 0.5.2 - 2026-07-10
11
+
12
+ - Fixed webinar create/duplicate output schemas so successful writes cannot be reported as protocol failures.
13
+ - Made Firestore event creation compensate automatically when the required RTDB mirror fails.
14
+ - Removed a stale RTDB mirror rewrite from metadata updates.
15
+ - Routed event deletion through an authorized server callable that cleans Firestore subcollections, both RTDB instances, Storage, participants, active sessions, and scheduled Telegram posts.
16
+ - Added a repeatable production smoke workflow against an exact published npm version.
17
+
3
18
  ## 0.5.1 - 2026-07-10
4
19
 
5
20
  - Replaced hand-maintained remote-image IP ranges with a tested public-unicast policy covering IPv4, IPv6, mapped, reserved, documentation, benchmark, multicast, and private address classes.
package/README.md CHANGED
@@ -52,6 +52,10 @@ OKO_IMAGE_ROOTS=/trusted/assets:/another/trusted/folder
52
52
 
53
53
  Node.js 20.18.1 or newer is required.
54
54
 
55
+ ## What OKO Does
56
+
57
+ OKO is a webinar experience and operations layer, not a video-streaming host. The organizer supplies a YouTube, iframe, or other player URL; OKO adds the event page, configurable audience blocks, chat, analytics, and NMO attendance controls around it. Start/End tools control analytics sessions only, never the underlying video stream.
58
+
55
59
  ## Security Model
56
60
 
57
61
  - Each API key exchanges into its own Firebase identity `mcp_<keyId>`; it never inherits platform-owner claims from the organizer account.
@@ -78,7 +82,7 @@ Every tool returns JSON text plus MCP `structuredContent`. List-like tools use b
78
82
  The server also exposes MCP-native onboarding:
79
83
 
80
84
  - `instructions` on initialize with the supported webinar creation flow and product invariants.
81
- - Resources: `oko://guide/create-webinar`, `oko://guide/platform`, `oko://guide/security`, `oko://schema/event`, `oko://capabilities`.
85
+ - Resources: `oko://guide/create-webinar`, `oko://guide/platform`, `oko://guide/security`, `oko://guide/nmo`, `oko://schema/event`, `oko://guide/telegram`, `oko://capabilities`.
82
86
  - Prompts: `create_webinar_from_brief`, `audit_event_setup`, `prepare_session_report`.
83
87
 
84
88
  This package is published as source-available runtime distribution for OKO customers. `license: UNLICENSED` means all rights reserved unless a separate agreement says otherwise.
@@ -157,7 +161,7 @@ Dangerous or external side-effect tools require `confirm: true`.
157
161
  10. `oko_get_session_report` should return markdown and structured session data for a completed session.
158
162
  11. `oko_list_notifications` -> `oko_mark_notifications_read` should read and clear an own disposable notification.
159
163
  12. `oko_get_analytics` should read an existing event with analytics sessions.
160
- 13. MCP protocol smoke should report 59 tools, 5 resources, 3 prompts, and non-empty initialize instructions.
164
+ 13. MCP protocol smoke should report 59 tools, 7 resources, 3 prompts, and non-empty initialize instructions.
161
165
 
162
166
  CLI help:
163
167
 
package/dist/guides.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
2
  export const OKO_MCP_INSTRUCTIONS = [
3
3
  "You are controlling OKO Stream as an organizer through MCP tools.",
4
+ "OKO is a webinar experience and operations layer, not a video-streaming host. The organizer supplies a YouTube, iframe, or other player URL; OKO adds the event page, audience blocks, chat, analytics, and NMO attendance controls around it.",
5
+ "Use the source player or encoder to start video. In OKO, Start and End manage analytics sessions only, while blocks and chat control the attendee experience around the supplied video.",
4
6
  "Start every new workspace session with oko_whoami, then respect the returned scopes.",
5
7
  "OKO events have no draft/live/ended status. Start/End controls manage analytics sessions only, never video streaming.",
6
8
  "For webinar creation, prefer this flow: oko_create_webinar, oko_configure_blocks, oko_configure_cards, image uploads, oko_validate_event_setup, then return public/setup/analytics links.",
@@ -41,6 +43,9 @@ const GUIDE_RESOURCES = [
41
43
  text: [
42
44
  "# OKO Platform Invariants",
43
45
  "",
46
+ "## Product model",
47
+ "OKO does not host or encode video. An organizer brings a video source such as YouTube or an iframe player. OKO builds the webinar experience around that source: an event page, configurable blocks, cards, forms, chat, analytics, and NMO attendance controls. Starting the source video happens outside OKO; starting an OKO session starts analytics only.",
48
+ "",
44
49
  "- OKO events have no `draft`, `live`, or `ended` status field.",
45
50
  "- Public event pages render whenever the event exists and normal access rules allow it.",
46
51
  "- Start and End controls manage analytics sessions only; they never start or stop the video stream.",
@@ -50,6 +55,33 @@ const GUIDE_RESOURCES = [
50
55
  "- Image variants are asynchronous. A master upload is not fully ready until `imageVariants` are present.",
51
56
  ].join("\n"),
52
57
  },
58
+ {
59
+ name: "oko_mcp_nmo",
60
+ uri: "oko://guide/nmo",
61
+ title: "Run an NMO / CME Webinar",
62
+ description: "Complete organizer workflow for NMO participants, checkpoints, and technical attendance results.",
63
+ text: [
64
+ "# Run an NMO / CME Webinar",
65
+ "",
66
+ "NMO mode is for attendance-controlled medical education webinars. OKO records technical eligibility from watch-time and checkpoint ratios; the organizer remains responsible for the formal accreditation process and participant communications.",
67
+ "",
68
+ "## Prepare",
69
+ "1. Create the event with its video source, then call `oko_configure_blocks` with `nmo.enabled:true`, watch-time and checkpoint criteria, and optional position/organization fields.",
70
+ "2. Add approved participants with `oko_add_nmo_participant` or bulk-import them with `oko_import_nmo_participants`. Inspect per-row import failures before continuing.",
71
+ "3. Return the event's registration and login links to participants. NMO access is gated by the imported participant list.",
72
+ "4. Call `oko_validate_event_setup`; an NMO event without participants or valid criteria is not ready.",
73
+ "",
74
+ "## During the webinar",
75
+ "1. Call `oko_start_session` when attendance analytics should begin. This does not start the supplied video stream.",
76
+ "2. At the organizer's explicit request, call `oko_launch_checkpoint` with `confirm:true`, then call `oko_close_checkpoint` with fresh confirmation when the response window must close.",
77
+ "3. Use `oko_get_live_state` and participant/analytics reads to monitor the live session. Never launch or close a checkpoint silently.",
78
+ "",
79
+ "## After the webinar",
80
+ "1. End the analytics session only after explicit organizer confirmation.",
81
+ "2. Use `oko_get_session_report` or `oko_get_analytics` with NMO rows when detailed technical attendance results are needed.",
82
+ "3. Report watch-time and checkpoint results as technical OKO evidence. Do not promise an official credit or certificate unless the organizer's accreditation process confirms it.",
83
+ ].join("\n"),
84
+ },
53
85
  {
54
86
  name: "oko_mcp_security",
55
87
  uri: "oko://guide/security",
@@ -109,9 +141,36 @@ const GUIDE_RESOURCES = [
109
141
  "- `nmoMode`, `nmoCriteria`, `participantUids`: NMO mode and participant gates.",
110
142
  "- `workspaceId` plus capability arrays (`accessibleBy`, `editors`, `deleters`, `liveControllers`, `chatModerators`, `sessionManagers`, `analyticsViewers`, `reportDownloaders`, `notificationSenders`) are Admin-owned access mirrors.",
111
143
  "",
144
+ "## Button targets and forms",
145
+ "A `formId` on a CTA, banner, or card button opens that event-level lead form in a popup instead of navigating away. Use `buttonUrl` only when the button should navigate to a URL. Although legacy data can contain both, set exactly one target in new MCP writes; `formId` takes precedence in the attendee UI.",
146
+ "",
112
147
  "Do not remove unknown fields or write access mirrors. Use dedicated MCP tools instead of whole-document rewrites.",
113
148
  ].join("\n"),
114
149
  },
150
+ {
151
+ name: "oko_mcp_telegram",
152
+ uri: "oko://guide/telegram",
153
+ title: "Connect Telegram and Publish Webinar Posts",
154
+ description: "Claim a Telegram channel, connect it to an event, and publish or schedule posts safely.",
155
+ text: [
156
+ "# Connect Telegram and Publish Webinar Posts",
157
+ "",
158
+ "## Claim a channel",
159
+ "1. Add `@OKOStreamBot` to the Telegram channel as an administrator with permission to post.",
160
+ "2. The bot provides a claim code for that channel. Copy it exactly; do not invent a channel ID.",
161
+ "3. Call `oko_claim_telegram_channel` with the claim code, optional workspace ids, and `confirm:true`.",
162
+ "4. Call `oko_list_telegram_channels` and choose a channel by its returned title and id.",
163
+ "5. Call `oko_configure_blocks` with `telegramChannelIds` to connect the claimed channel to an event.",
164
+ "",
165
+ "## Publish safely",
166
+ "1. Use `oko_publish_telegram_post` with the event id, returned channel id, caption, optional image/button, and `confirm:true`.",
167
+ "2. Include `scheduleAt` for a scheduled post. Without it, the tool sends immediately to the real channel, so show the exact channel and caption before confirmation.",
168
+ "3. Use `oko_list_telegram_posts` to inspect draft, scheduled, sent, or failed posts. Only cancel draft, scheduled, or failed posts with `oko_cancel_telegram_post` and `confirm:true`.",
169
+ "",
170
+ "## Automatic reminders",
171
+ "OKO has a platform auto-reminder job for opted-in events. The current MCP surface does not toggle `telegram.autoReminder`, so an MCP-only workflow should create a scheduled reminder post explicitly. Use the web setup if the organizer specifically needs the platform auto-reminder setting.",
172
+ ].join("\n"),
173
+ },
115
174
  {
116
175
  name: "oko_mcp_capabilities",
117
176
  uri: "oko://capabilities",
@@ -134,6 +193,12 @@ const GUIDE_RESOURCES = [
134
193
  "- Manage workspaces, members, invites, notifications, and Telegram post lifecycle.",
135
194
  "- Send organizer chat messages and moderate chat users when scoped.",
136
195
  "- Import/remove NMO participants through scoped Cloud Functions.",
196
+ "",
197
+ "## Outside the MCP boundary",
198
+ "- OKO MCP does not host, encode, start, or stop the underlying video stream; configure that in the source player or encoder.",
199
+ "- It does not create organizer accounts, manage pricing or payments, or expose arbitrary Firebase administration.",
200
+ "- It does not provide a general theme designer. Use the web product or support for platform-level branding and account administration.",
201
+ "- It does not currently toggle the platform's Telegram auto-reminder setting; schedule a post or use the web setup instead.",
137
202
  ].join("\n"),
138
203
  },
139
204
  ];
package/dist/server.js CHANGED
@@ -23,7 +23,7 @@ if (process.argv.includes("--help")) {
23
23
  }
24
24
  const server = new McpServer({
25
25
  name: "oko-mcp-server",
26
- version: "0.5.1",
26
+ version: "0.5.3",
27
27
  }, {
28
28
  instructions: OKO_MCP_INSTRUCTIONS,
29
29
  });
package/dist/tools.js CHANGED
@@ -77,11 +77,21 @@ const updateOutputSchema = z.object({
77
77
  eventId: z.string().optional(),
78
78
  updated: z.array(z.string()).optional(),
79
79
  }).passthrough();
80
- const createWebinarOutputSchema = {
81
- id: z.string(),
80
+ const eventLinksOutputShape = {
82
81
  url: z.string().url(),
83
- workspaceId: z.string(),
82
+ setupUrl: z.string().url(),
83
+ analyticsUrl: z.string().url(),
84
+ registrationUrl: z.string().url(),
85
+ loginUrl: z.string().url(),
86
+ nmoRegistrationUrl: z.string().url().optional(),
87
+ nmoLoginUrl: z.string().url().optional(),
84
88
  };
89
+ const createWebinarOutputSchema = z.object({
90
+ id: z.string(),
91
+ workspaceId: z.string(),
92
+ sourceEventId: z.string().optional(),
93
+ ...eventLinksOutputShape,
94
+ }).passthrough();
85
95
  const cursorMetaSchema = {
86
96
  count: z.number().int().nonnegative(),
87
97
  limit: z.number().int().positive(),
@@ -100,7 +110,7 @@ const eventsOutputSchema = z.object({
100
110
  }).passthrough();
101
111
  const eventOutputSchema = z.object({
102
112
  id: z.string(),
103
- url: z.string().url(),
113
+ ...eventLinksOutputShape,
104
114
  }).passthrough();
105
115
  const liveStateOutputSchema = z.object({
106
116
  eventId: z.string(),
@@ -1099,6 +1109,22 @@ function initialEventAccess(uid) {
1099
1109
  notificationSenders: [uid],
1100
1110
  };
1101
1111
  }
1112
+ async function addEventWithRtdbMirror(uid, workspaceId, eventData) {
1113
+ const eventRef = await addDoc(collection(firestore, "events"), eventData);
1114
+ try {
1115
+ await update(ref(rtdb, `events/${eventRef.id}`), { ownerId: uid, workspaceId });
1116
+ }
1117
+ catch (error) {
1118
+ try {
1119
+ await deleteDoc(eventRef);
1120
+ }
1121
+ catch (rollbackError) {
1122
+ throw new AggregateError([error, rollbackError], `RTDB mirror failed for event ${eventRef.id}, and Firestore rollback also failed.`);
1123
+ }
1124
+ throw error;
1125
+ }
1126
+ return eventRef;
1127
+ }
1102
1128
  async function assertCanCreateInWorkspace(uid, workspaceId) {
1103
1129
  if (workspaceId === uid)
1104
1130
  return;
@@ -2040,7 +2066,7 @@ export function registerTools(server) {
2040
2066
  const workspaceId = args.workspaceId ?? uid;
2041
2067
  await assertCanCreateInWorkspace(uid, workspaceId);
2042
2068
  const video = normalizeVideo(args.videoUrl);
2043
- const docRef = await addDoc(collection(firestore, "events"), {
2069
+ const docRef = await addEventWithRtdbMirror(uid, workspaceId, {
2044
2070
  ownerId: uid,
2045
2071
  workspaceId,
2046
2072
  ...initialEventAccess(uid),
@@ -2058,10 +2084,6 @@ export function registerTools(server) {
2058
2084
  promoBanner: { enabled: false, text: "", link: "", visibility: "always" },
2059
2085
  },
2060
2086
  });
2061
- await update(ref(rtdb, `events/${docRef.id}`), {
2062
- ownerId: uid,
2063
- workspaceId,
2064
- });
2065
2087
  return textResult({ id: docRef.id, ...eventLinks(docRef.id), workspaceId });
2066
2088
  });
2067
2089
  server.registerTool("oko_update_event", {
@@ -2096,7 +2118,7 @@ export function registerTools(server) {
2096
2118
  if (requiredScopes.size === 0)
2097
2119
  throw new Error("Provide at least one field to update.");
2098
2120
  const { uid } = await authed([...requiredScopes]);
2099
- const data = await readEvent(args.eventId, uid, "edit");
2121
+ await readEvent(args.eventId, uid, "edit");
2100
2122
  const patch = {};
2101
2123
  if (args.title !== undefined)
2102
2124
  patch.title = args.title.trim();
@@ -2114,10 +2136,6 @@ export function registerTools(server) {
2114
2136
  if (args.pinned !== undefined)
2115
2137
  patch.pinned = args.pinned;
2116
2138
  await updateDoc(doc(firestore, "events", args.eventId), patch);
2117
- await update(ref(rtdb, `events/${args.eventId}`), {
2118
- ownerId: safeString(data.ownerId, uid),
2119
- workspaceId: safeString(data.workspaceId, uid),
2120
- });
2121
2139
  return textResult({ id: args.eventId, updated: Object.keys(patch) });
2122
2140
  });
2123
2141
  server.registerTool("oko_configure_blocks", {
@@ -2499,11 +2517,11 @@ export function registerTools(server) {
2499
2517
  server.registerTool("oko_delete_event", {
2500
2518
  title: "Delete event",
2501
2519
  description: toolDescription({
2502
- summary: "Delete an OKO event document.",
2520
+ summary: "Delete an OKO event and its associated Firestore, RTDB, chat, Storage, participant, and scheduled-post data.",
2503
2521
  args: ["eventId: event to delete.", "confirm: must be true."],
2504
- returns: "deleted event id.",
2522
+ returns: "deleted event id and cross-store cleanup counts.",
2505
2523
  examples: ["Use only after the user explicitly confirms the exact event title/id."],
2506
- notes: ["Destructive. Does not control stream state; it deletes the event record."],
2524
+ notes: ["Destructive. The server validates the current per-event delete capability and performs idempotent cross-store cleanup."],
2507
2525
  }),
2508
2526
  inputSchema: { eventId: eventIdField, confirm: z.boolean().optional().describe("Required true for deletion.") },
2509
2527
  outputSchema: z.object({ deleted: z.string() }).passthrough(),
@@ -2512,8 +2530,9 @@ export function registerTools(server) {
2512
2530
  const { uid } = await authed(["events:delete"]);
2513
2531
  const data = await readEvent(eventId, uid, "delete");
2514
2532
  requireConfirm(confirm, "oko_delete_event", { eventId, title: data.title });
2515
- await deleteDoc(doc(firestore, "events", eventId));
2516
- return textResult({ deleted: eventId });
2533
+ const deleteEvent = httpsCallable(functions, "deleteEventCompletely");
2534
+ const result = await deleteEvent({ eventId });
2535
+ return textResult(result.data);
2517
2536
  });
2518
2537
  server.registerTool("oko_duplicate_event", {
2519
2538
  title: "Duplicate event",
@@ -2535,7 +2554,7 @@ export function registerTools(server) {
2535
2554
  const source = await readEvent(eventId, uid, "read");
2536
2555
  const workspaceId = targetWorkspaceId ?? safeString(source.workspaceId, uid);
2537
2556
  await assertCanCreateInWorkspace(uid, workspaceId);
2538
- const newRef = await addDoc(collection(firestore, "events"), {
2557
+ const newRef = await addEventWithRtdbMirror(uid, workspaceId, {
2539
2558
  ownerId: uid,
2540
2559
  workspaceId,
2541
2560
  ...initialEventAccess(uid),
@@ -2551,7 +2570,6 @@ export function registerTools(server) {
2551
2570
  ...(source.nmoCriteria && typeof source.nmoCriteria === "object" ? { nmoCriteria: source.nmoCriteria } : {}),
2552
2571
  ...(source.nmoExtraFields && typeof source.nmoExtraFields === "object" ? { nmoExtraFields: source.nmoExtraFields } : {}),
2553
2572
  });
2554
- await update(ref(rtdb, `events/${newRef.id}`), { ownerId: uid, workspaceId });
2555
2573
  return textResult({ id: newRef.id, sourceEventId: eventId, workspaceId, ...eventLinks(newRef.id) });
2556
2574
  });
2557
2575
  server.registerTool("oko_move_event", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okostream/mcp",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "MCP server for managing OKO Stream webinars as an organizer.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -26,6 +26,7 @@
26
26
  "test": "npm run build && node --test test/*.test.mjs",
27
27
  "eval:contract": "npm run build && node eval/contract-eval.mjs",
28
28
  "eval:model": "npm run build && node eval/model-eval.mjs",
29
+ "smoke:prod": "node eval/prod-smoke.mjs",
29
30
  "prepublishOnly": "npm run build",
30
31
  "start": "node dist/server.js"
31
32
  },