@okostream/mcp 0.9.2 → 0.9.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/dist/tools.js +70 -30
- package/dist/utils.js +0 -3
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -868,13 +868,6 @@ function eventLinks(eventId) {
|
|
|
868
868
|
analyticsUrl: analyticsUrl(eventId),
|
|
869
869
|
};
|
|
870
870
|
}
|
|
871
|
-
function assertEventReadable(data, uid) {
|
|
872
|
-
const access = Array.isArray(data.accessibleBy) ? data.accessibleBy : [];
|
|
873
|
-
const editors = Array.isArray(data.editors) ? data.editors : [];
|
|
874
|
-
if (data.ownerId !== uid && !access.includes(uid) && !editors.includes(uid)) {
|
|
875
|
-
throw new Error("Event is not accessible to this organizer.");
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
871
|
const EVENT_ACCESS_FIELD_BY_MODE = {
|
|
879
872
|
read: "accessibleBy",
|
|
880
873
|
edit: "editors",
|
|
@@ -886,22 +879,44 @@ const EVENT_ACCESS_FIELD_BY_MODE = {
|
|
|
886
879
|
moderate: "chatModerators",
|
|
887
880
|
notify: "notificationSenders",
|
|
888
881
|
};
|
|
889
|
-
function
|
|
882
|
+
function grantAllowsMode(data, mode) {
|
|
890
883
|
const field = EVENT_ACCESS_FIELD_BY_MODE[mode];
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
884
|
+
return asRecord(data.capabilities)[field] === true;
|
|
885
|
+
}
|
|
886
|
+
function legacyEventAllowsMode(data, uid, mode) {
|
|
887
|
+
if (data.ownerId === uid)
|
|
888
|
+
return true;
|
|
889
|
+
const fields = mode === "read"
|
|
890
|
+
? ["accessibleBy", "editors"]
|
|
891
|
+
: [EVENT_ACCESS_FIELD_BY_MODE[mode]];
|
|
892
|
+
return fields.some((field) => (Array.isArray(data[field]) && data[field].includes(uid)));
|
|
893
|
+
}
|
|
894
|
+
function isFirestorePermissionDenied(error) {
|
|
895
|
+
if (!error || typeof error !== "object")
|
|
896
|
+
return false;
|
|
897
|
+
const code = error.code;
|
|
898
|
+
return code === "permission-denied"
|
|
899
|
+
|| code === "firestore/permission-denied"
|
|
900
|
+
|| code === "PERMISSION_DENIED";
|
|
895
901
|
}
|
|
896
902
|
async function readEvent(eventId, uid, mode = "read") {
|
|
897
|
-
const snap = await
|
|
903
|
+
const [snap, grant] = await Promise.all([
|
|
904
|
+
getDoc(doc(firestore, "events", eventId)),
|
|
905
|
+
getDoc(doc(firestore, "eventAccess", uid, "grants", eventId)).catch((error) => {
|
|
906
|
+
if (!isFirestorePermissionDenied(error))
|
|
907
|
+
throw error;
|
|
908
|
+
return null;
|
|
909
|
+
}),
|
|
910
|
+
]);
|
|
898
911
|
if (!snap.exists())
|
|
899
912
|
throw new Error("Event not found.");
|
|
900
913
|
const data = snap.data();
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
914
|
+
const allowed = grant === null
|
|
915
|
+
? legacyEventAllowsMode(data, uid, mode)
|
|
916
|
+
: data.ownerId === uid || (grant.exists() && grantAllowsMode(grant.data(), mode));
|
|
917
|
+
if (!allowed) {
|
|
918
|
+
throw new Error(`Event does not grant ${mode} access to this organizer.`);
|
|
919
|
+
}
|
|
905
920
|
if (data.trashedAt && mode !== "read" && mode !== "delete") {
|
|
906
921
|
throw new Error("Event is in trash. Restore it before making changes or live actions.");
|
|
907
922
|
}
|
|
@@ -1047,7 +1062,7 @@ function initialEventAccess(uid) {
|
|
|
1047
1062
|
notificationSenders: [uid],
|
|
1048
1063
|
};
|
|
1049
1064
|
}
|
|
1050
|
-
async function addEventWithRtdbMirror(
|
|
1065
|
+
async function addEventWithRtdbMirror(eventData) {
|
|
1051
1066
|
const eventRef = await addDoc(collection(firestore, "events"), eventData);
|
|
1052
1067
|
try {
|
|
1053
1068
|
const ensureRuntime = httpsCallable(functions, "ensureEventRuntime");
|
|
@@ -1466,7 +1481,7 @@ export function registerTools(server) {
|
|
|
1466
1481
|
args: ["workspaceId: workspace owned by the organizer.", "uid: member uid.", "permissions: 9 workspace permission booleans.", "confirm: must be true."],
|
|
1467
1482
|
returns: "workspaceId, uid, and updated=true.",
|
|
1468
1483
|
examples: ["Use to change an existing member's permissions, or direct-add a known uid."],
|
|
1469
|
-
notes: ["Changing a share
|
|
1484
|
+
notes: ["Changing a share materializes lightweight event grants and updates RTDB workspace/chat mirrors."],
|
|
1470
1485
|
}),
|
|
1471
1486
|
inputSchema: {
|
|
1472
1487
|
workspaceId: z.string().min(1),
|
|
@@ -1498,7 +1513,7 @@ export function registerTools(server) {
|
|
|
1498
1513
|
args: ["workspaceId: workspace owned by the organizer.", "uid: member uid.", "confirm: must be true."],
|
|
1499
1514
|
returns: "workspaceId, uid, removed=true.",
|
|
1500
1515
|
examples: ["Use after listing workspace access and confirming the exact uid."],
|
|
1501
|
-
notes: ["Removing a share
|
|
1516
|
+
notes: ["Removing a share revokes its event grants and RTDB workspace/chat mirrors."],
|
|
1502
1517
|
}),
|
|
1503
1518
|
inputSchema: { workspaceId: z.string().min(1), uid: z.string().min(1), confirm: z.boolean().optional().describe("Required true for member removal.") },
|
|
1504
1519
|
outputSchema: z.object({ workspaceId: z.string(), uid: z.string(), removed: z.boolean() }).passthrough(),
|
|
@@ -1583,7 +1598,7 @@ export function registerTools(server) {
|
|
|
1583
1598
|
args: [
|
|
1584
1599
|
"limit: maximum events to return, default 30, max 100.",
|
|
1585
1600
|
"cursor: timestamp cursor returned as nextCursor by the previous page.",
|
|
1586
|
-
"workspaceId: optional
|
|
1601
|
+
"workspaceId: optional workspace filter.",
|
|
1587
1602
|
],
|
|
1588
1603
|
returns: "events plus count, limit, hasMore/has_more, nextCursor/next_cursor.",
|
|
1589
1604
|
examples: ["Use limit=10 first; if hasMore is true, call again with cursor=nextCursor."],
|
|
@@ -1598,17 +1613,42 @@ export function registerTools(server) {
|
|
|
1598
1613
|
}, async (args) => {
|
|
1599
1614
|
const { uid } = await authed(["events:read"]);
|
|
1600
1615
|
const constraints = [
|
|
1601
|
-
where("
|
|
1616
|
+
...(args.workspaceId ? [where("workspaceId", "==", args.workspaceId)] : []),
|
|
1602
1617
|
orderBy("createdAt", "desc"),
|
|
1603
1618
|
limit(args.limit + 1),
|
|
1604
1619
|
];
|
|
1605
1620
|
if (args.cursor)
|
|
1606
|
-
constraints.splice(
|
|
1607
|
-
const snap = await getDocs(query(collection(firestore, "
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1621
|
+
constraints.splice(constraints.length - 1, 0, startAfter(parseTimestampCursor(args.cursor)));
|
|
1622
|
+
const snap = await getDocs(query(collection(firestore, "eventAccess", uid, "grants"), ...constraints)).catch((error) => {
|
|
1623
|
+
if (!isFirestorePermissionDenied(error))
|
|
1624
|
+
throw error;
|
|
1625
|
+
return null;
|
|
1626
|
+
});
|
|
1627
|
+
if (snap !== null) {
|
|
1628
|
+
const { page, hasMore } = pickPage(snap.docs, args.limit);
|
|
1629
|
+
const eventDocuments = await Promise.all(page.map((item) => (getDoc(doc(firestore, "events", item.id)))));
|
|
1630
|
+
const events = eventDocuments.flatMap((item) => (item.exists()
|
|
1631
|
+
? [{ id: item.id, ...serialize(item.data()) }]
|
|
1632
|
+
: []));
|
|
1633
|
+
const nextCursor = hasMore ? timestampCursorFrom(page.at(-1)?.data().createdAt) : null;
|
|
1634
|
+
return textResult({ events, ...cursorMeta(events.length, args.limit, hasMore, nextCursor) });
|
|
1635
|
+
}
|
|
1636
|
+
const legacyConstraints = [
|
|
1637
|
+
where("accessibleBy", "array-contains", uid),
|
|
1638
|
+
orderBy("createdAt", "desc"),
|
|
1639
|
+
limit(args.limit + 1),
|
|
1640
|
+
];
|
|
1641
|
+
if (args.cursor) {
|
|
1642
|
+
legacyConstraints.splice(legacyConstraints.length - 1, 0, startAfter(parseTimestampCursor(args.cursor)));
|
|
1643
|
+
}
|
|
1644
|
+
const legacySnap = await getDocs(query(collection(firestore, "events"), ...legacyConstraints));
|
|
1645
|
+
const { page, hasMore } = pickPage(legacySnap.docs, args.limit);
|
|
1646
|
+
const events = page
|
|
1647
|
+
.map((item) => ({
|
|
1648
|
+
id: item.id,
|
|
1649
|
+
...serialize(item.data()),
|
|
1650
|
+
}))
|
|
1651
|
+
.filter((event) => !args.workspaceId || event.workspaceId === args.workspaceId);
|
|
1612
1652
|
const nextCursor = hasMore ? timestampCursorFrom(page.at(-1)?.data().createdAt) : null;
|
|
1613
1653
|
return textResult({ events, ...cursorMeta(events.length, args.limit, hasMore, nextCursor) });
|
|
1614
1654
|
});
|
|
@@ -1679,7 +1719,7 @@ export function registerTools(server) {
|
|
|
1679
1719
|
const workspaceId = args.workspaceId ?? uid;
|
|
1680
1720
|
await assertCanCreateInWorkspace(uid, workspaceId);
|
|
1681
1721
|
const video = normalizeVideo(args.videoUrl);
|
|
1682
|
-
const docRef = await addEventWithRtdbMirror(
|
|
1722
|
+
const docRef = await addEventWithRtdbMirror({
|
|
1683
1723
|
ownerId: uid,
|
|
1684
1724
|
workspaceId,
|
|
1685
1725
|
...initialEventAccess(uid),
|
|
@@ -2222,7 +2262,7 @@ export function registerTools(server) {
|
|
|
2222
2262
|
const source = await readEvent(eventId, uid, "read");
|
|
2223
2263
|
const workspaceId = targetWorkspaceId ?? safeString(source.workspaceId, uid);
|
|
2224
2264
|
await assertCanCreateInWorkspace(uid, workspaceId);
|
|
2225
|
-
const newRef = await addEventWithRtdbMirror(
|
|
2265
|
+
const newRef = await addEventWithRtdbMirror({
|
|
2226
2266
|
ownerId: uid,
|
|
2227
2267
|
workspaceId,
|
|
2228
2268
|
...initialEventAccess(uid),
|
package/dist/utils.js
CHANGED
|
@@ -19,9 +19,6 @@ export function serialize(value) {
|
|
|
19
19
|
}
|
|
20
20
|
return out;
|
|
21
21
|
}
|
|
22
|
-
export function docJson(snap) {
|
|
23
|
-
return { id: snap.id, ...serialize(snap.data()) };
|
|
24
|
-
}
|
|
25
22
|
function compactForText(value, depth = 0) {
|
|
26
23
|
if (typeof value === "string" && value.length > MAX_COMPACT_STRING_LENGTH) {
|
|
27
24
|
return `${value.slice(0, MAX_COMPACT_STRING_LENGTH)}... [truncated ${value.length - MAX_COMPACT_STRING_LENGTH} chars]`;
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/generateMcpVersion.mjs from mcp/package.json. Do not edit.
|
|
2
|
-
export const OKO_MCP_VERSION = "0.9.
|
|
2
|
+
export const OKO_MCP_VERSION = "0.9.3";
|