@okostream/mcp 0.9.1 → 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/env.js +22 -9
- package/dist/sessionRepair.js +37 -0
- package/dist/tools.js +127 -45
- package/dist/utils.js +0 -3
- package/dist/version.js +2 -1
- package/package.json +2 -2
package/dist/env.js
CHANGED
|
@@ -3,9 +3,16 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { delimiter, isAbsolute, join, parse, resolve } from "node:path";
|
|
4
4
|
import { DEFAULT_EXCHANGE_URL, DEFAULT_FIREBASE_CONFIG } from "./firebaseDefaults.js";
|
|
5
5
|
config({ path: join(homedir(), ".env"), quiet: true });
|
|
6
|
+
const USER_CONFIG_PLACEHOLDER = /^\$\{user_config\.[^{}\s]+\}$/;
|
|
7
|
+
function normalizeOptionalEnv(value) {
|
|
8
|
+
const normalized = value?.trim();
|
|
9
|
+
if (!normalized || USER_CONFIG_PLACEHOLDER.test(normalized))
|
|
10
|
+
return null;
|
|
11
|
+
return normalized;
|
|
12
|
+
}
|
|
6
13
|
function readOptionalEnv(...names) {
|
|
7
14
|
for (const name of names) {
|
|
8
|
-
const value = process.env[name]
|
|
15
|
+
const value = normalizeOptionalEnv(process.env[name]);
|
|
9
16
|
if (value)
|
|
10
17
|
return value;
|
|
11
18
|
}
|
|
@@ -13,33 +20,39 @@ function readOptionalEnv(...names) {
|
|
|
13
20
|
}
|
|
14
21
|
function readEnvWithDefault(defaultValue, ...names) {
|
|
15
22
|
for (const name of names) {
|
|
16
|
-
const value = process.env[name]
|
|
23
|
+
const value = normalizeOptionalEnv(process.env[name]);
|
|
17
24
|
if (value)
|
|
18
25
|
return value;
|
|
19
26
|
}
|
|
20
27
|
return defaultValue;
|
|
21
28
|
}
|
|
29
|
+
function disableImageRoots(reason) {
|
|
30
|
+
console.error(`[oko-mcp] Local image reads disabled: ${reason}`);
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
22
33
|
function readImageRoots() {
|
|
23
|
-
const rawRoots =
|
|
34
|
+
const rawRoots = readOptionalEnv("OKO_IMAGE_ROOTS");
|
|
24
35
|
if (!rawRoots)
|
|
25
36
|
return [];
|
|
26
|
-
|
|
37
|
+
const roots = [];
|
|
38
|
+
for (const value of rawRoots.split(delimiter)) {
|
|
27
39
|
const root = value.trim();
|
|
28
40
|
if (!root) {
|
|
29
|
-
|
|
41
|
+
return disableImageRoots("OKO_IMAGE_ROOTS contains an empty directory entry.");
|
|
30
42
|
}
|
|
31
43
|
const expanded = root === "~" || root.startsWith("~/")
|
|
32
44
|
? join(homedir(), root.slice(2))
|
|
33
45
|
: root;
|
|
34
46
|
if (!isAbsolute(expanded)) {
|
|
35
|
-
|
|
47
|
+
return disableImageRoots("OKO_IMAGE_ROOTS entries must be absolute paths.");
|
|
36
48
|
}
|
|
37
49
|
const resolved = resolve(expanded);
|
|
38
50
|
if (resolved === parse(resolved).root) {
|
|
39
|
-
|
|
51
|
+
return disableImageRoots("OKO_IMAGE_ROOTS must not allow an entire filesystem root.");
|
|
40
52
|
}
|
|
41
|
-
|
|
42
|
-
}
|
|
53
|
+
roots.push(resolved);
|
|
54
|
+
}
|
|
55
|
+
return roots;
|
|
43
56
|
}
|
|
44
57
|
export const env = {
|
|
45
58
|
apiKey: readOptionalEnv("OKO_API_KEY"),
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function summarizeSessionRepair(input) {
|
|
2
|
+
const fixed = !input.verificationError
|
|
3
|
+
&& input.firestorePointer === null
|
|
4
|
+
&& input.rtdbPointer === null;
|
|
5
|
+
const fulfilled = new Set(input.attempts
|
|
6
|
+
.filter((attempt) => attempt.status === "fulfilled")
|
|
7
|
+
.map((attempt) => attempt.path));
|
|
8
|
+
const cleared = [
|
|
9
|
+
...(input.firestorePointer === null ? ["events.activeAnalyticsSessionId"] : []),
|
|
10
|
+
...(input.rtdbPointer === null ? ["ctrl/activeSessionId"] : []),
|
|
11
|
+
...(input.sessionId && fulfilled.has(`activeSessions/${input.sessionId}`)
|
|
12
|
+
? [`activeSessions/${input.sessionId}`]
|
|
13
|
+
: []),
|
|
14
|
+
...(input.sessionId
|
|
15
|
+
&& input.deleteRtdbSessionData
|
|
16
|
+
&& fulfilled.has(`events/${input.eventId}/sessions/${input.sessionId}`)
|
|
17
|
+
? [`events/${input.eventId}/sessions/${input.sessionId}`]
|
|
18
|
+
: []),
|
|
19
|
+
];
|
|
20
|
+
const warnings = input.attempts
|
|
21
|
+
.filter((attempt) => attempt.status === "rejected")
|
|
22
|
+
.map((attempt) => `${attempt.path}: ${attempt.error || "cleanup failed"}`);
|
|
23
|
+
if (input.verificationError) {
|
|
24
|
+
warnings.push(`postcondition verification: ${input.verificationError}`);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
eventId: input.eventId,
|
|
28
|
+
sessionId: input.sessionId,
|
|
29
|
+
fixed,
|
|
30
|
+
cleared,
|
|
31
|
+
warnings,
|
|
32
|
+
remainingPointers: {
|
|
33
|
+
firestore: input.firestorePointer,
|
|
34
|
+
rtdb: input.rtdbPointer,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
package/dist/tools.js
CHANGED
|
@@ -13,6 +13,7 @@ import { env } from "./env.js";
|
|
|
13
13
|
import { ipLiteralFamily, isPublicUnicastAddress, normalizeIpLiteralHostname, } from "./networkPolicy.js";
|
|
14
14
|
import { chatRtdb, firestore, functions, rtdb } from "./firebase.js";
|
|
15
15
|
import { OKO_CONNECTION_STATUSES } from "./onboarding.js";
|
|
16
|
+
import { summarizeSessionRepair } from "./sessionRepair.js";
|
|
16
17
|
import { canonicalTelegramChannelSchema, telegramChannelSchema, } from "./telegramChannelSchema.js";
|
|
17
18
|
import { csvCell, requireConfirm, safeString, serialize, textResult } from "./utils.js";
|
|
18
19
|
import { detectAndNormalize, extractIframeSrc } from "./videoUrl.js";
|
|
@@ -867,13 +868,6 @@ function eventLinks(eventId) {
|
|
|
867
868
|
analyticsUrl: analyticsUrl(eventId),
|
|
868
869
|
};
|
|
869
870
|
}
|
|
870
|
-
function assertEventReadable(data, uid) {
|
|
871
|
-
const access = Array.isArray(data.accessibleBy) ? data.accessibleBy : [];
|
|
872
|
-
const editors = Array.isArray(data.editors) ? data.editors : [];
|
|
873
|
-
if (data.ownerId !== uid && !access.includes(uid) && !editors.includes(uid)) {
|
|
874
|
-
throw new Error("Event is not accessible to this organizer.");
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
871
|
const EVENT_ACCESS_FIELD_BY_MODE = {
|
|
878
872
|
read: "accessibleBy",
|
|
879
873
|
edit: "editors",
|
|
@@ -885,22 +879,44 @@ const EVENT_ACCESS_FIELD_BY_MODE = {
|
|
|
885
879
|
moderate: "chatModerators",
|
|
886
880
|
notify: "notificationSenders",
|
|
887
881
|
};
|
|
888
|
-
function
|
|
882
|
+
function grantAllowsMode(data, mode) {
|
|
889
883
|
const field = EVENT_ACCESS_FIELD_BY_MODE[mode];
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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";
|
|
894
901
|
}
|
|
895
902
|
async function readEvent(eventId, uid, mode = "read") {
|
|
896
|
-
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
|
+
]);
|
|
897
911
|
if (!snap.exists())
|
|
898
912
|
throw new Error("Event not found.");
|
|
899
913
|
const data = snap.data();
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
+
}
|
|
904
920
|
if (data.trashedAt && mode !== "read" && mode !== "delete") {
|
|
905
921
|
throw new Error("Event is in trash. Restore it before making changes or live actions.");
|
|
906
922
|
}
|
|
@@ -1046,7 +1062,7 @@ function initialEventAccess(uid) {
|
|
|
1046
1062
|
notificationSenders: [uid],
|
|
1047
1063
|
};
|
|
1048
1064
|
}
|
|
1049
|
-
async function addEventWithRtdbMirror(
|
|
1065
|
+
async function addEventWithRtdbMirror(eventData) {
|
|
1050
1066
|
const eventRef = await addDoc(collection(firestore, "events"), eventData);
|
|
1051
1067
|
try {
|
|
1052
1068
|
const ensureRuntime = httpsCallable(functions, "ensureEventRuntime");
|
|
@@ -1465,7 +1481,7 @@ export function registerTools(server) {
|
|
|
1465
1481
|
args: ["workspaceId: workspace owned by the organizer.", "uid: member uid.", "permissions: 9 workspace permission booleans.", "confirm: must be true."],
|
|
1466
1482
|
returns: "workspaceId, uid, and updated=true.",
|
|
1467
1483
|
examples: ["Use to change an existing member's permissions, or direct-add a known uid."],
|
|
1468
|
-
notes: ["Changing a share
|
|
1484
|
+
notes: ["Changing a share materializes lightweight event grants and updates RTDB workspace/chat mirrors."],
|
|
1469
1485
|
}),
|
|
1470
1486
|
inputSchema: {
|
|
1471
1487
|
workspaceId: z.string().min(1),
|
|
@@ -1497,7 +1513,7 @@ export function registerTools(server) {
|
|
|
1497
1513
|
args: ["workspaceId: workspace owned by the organizer.", "uid: member uid.", "confirm: must be true."],
|
|
1498
1514
|
returns: "workspaceId, uid, removed=true.",
|
|
1499
1515
|
examples: ["Use after listing workspace access and confirming the exact uid."],
|
|
1500
|
-
notes: ["Removing a share
|
|
1516
|
+
notes: ["Removing a share revokes its event grants and RTDB workspace/chat mirrors."],
|
|
1501
1517
|
}),
|
|
1502
1518
|
inputSchema: { workspaceId: z.string().min(1), uid: z.string().min(1), confirm: z.boolean().optional().describe("Required true for member removal.") },
|
|
1503
1519
|
outputSchema: z.object({ workspaceId: z.string(), uid: z.string(), removed: z.boolean() }).passthrough(),
|
|
@@ -1582,7 +1598,7 @@ export function registerTools(server) {
|
|
|
1582
1598
|
args: [
|
|
1583
1599
|
"limit: maximum events to return, default 30, max 100.",
|
|
1584
1600
|
"cursor: timestamp cursor returned as nextCursor by the previous page.",
|
|
1585
|
-
"workspaceId: optional
|
|
1601
|
+
"workspaceId: optional workspace filter.",
|
|
1586
1602
|
],
|
|
1587
1603
|
returns: "events plus count, limit, hasMore/has_more, nextCursor/next_cursor.",
|
|
1588
1604
|
examples: ["Use limit=10 first; if hasMore is true, call again with cursor=nextCursor."],
|
|
@@ -1597,17 +1613,42 @@ export function registerTools(server) {
|
|
|
1597
1613
|
}, async (args) => {
|
|
1598
1614
|
const { uid } = await authed(["events:read"]);
|
|
1599
1615
|
const constraints = [
|
|
1600
|
-
where("
|
|
1616
|
+
...(args.workspaceId ? [where("workspaceId", "==", args.workspaceId)] : []),
|
|
1601
1617
|
orderBy("createdAt", "desc"),
|
|
1602
1618
|
limit(args.limit + 1),
|
|
1603
1619
|
];
|
|
1604
1620
|
if (args.cursor)
|
|
1605
|
-
constraints.splice(
|
|
1606
|
-
const snap = await getDocs(query(collection(firestore, "
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
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);
|
|
1611
1652
|
const nextCursor = hasMore ? timestampCursorFrom(page.at(-1)?.data().createdAt) : null;
|
|
1612
1653
|
return textResult({ events, ...cursorMeta(events.length, args.limit, hasMore, nextCursor) });
|
|
1613
1654
|
});
|
|
@@ -1678,7 +1719,7 @@ export function registerTools(server) {
|
|
|
1678
1719
|
const workspaceId = args.workspaceId ?? uid;
|
|
1679
1720
|
await assertCanCreateInWorkspace(uid, workspaceId);
|
|
1680
1721
|
const video = normalizeVideo(args.videoUrl);
|
|
1681
|
-
const docRef = await addEventWithRtdbMirror(
|
|
1722
|
+
const docRef = await addEventWithRtdbMirror({
|
|
1682
1723
|
ownerId: uid,
|
|
1683
1724
|
workspaceId,
|
|
1684
1725
|
...initialEventAccess(uid),
|
|
@@ -2221,7 +2262,7 @@ export function registerTools(server) {
|
|
|
2221
2262
|
const source = await readEvent(eventId, uid, "read");
|
|
2222
2263
|
const workspaceId = targetWorkspaceId ?? safeString(source.workspaceId, uid);
|
|
2223
2264
|
await assertCanCreateInWorkspace(uid, workspaceId);
|
|
2224
|
-
const newRef = await addEventWithRtdbMirror(
|
|
2265
|
+
const newRef = await addEventWithRtdbMirror({
|
|
2225
2266
|
ownerId: uid,
|
|
2226
2267
|
workspaceId,
|
|
2227
2268
|
...initialEventAccess(uid),
|
|
@@ -2404,24 +2445,65 @@ export function registerTools(server) {
|
|
|
2404
2445
|
const sessionId = safeString(event.activeAnalyticsSessionId) || safeString(ctrlSnap.val());
|
|
2405
2446
|
requireConfirm(confirm, "oko_fix_orphan_session", { eventId, sessionId: sessionId || null, deleteRtdbSessionData });
|
|
2406
2447
|
const cleanup = [
|
|
2407
|
-
|
|
2408
|
-
|
|
2448
|
+
{
|
|
2449
|
+
path: "events.activeAnalyticsSessionId",
|
|
2450
|
+
promise: updateDoc(doc(firestore, "events", eventId), { activeAnalyticsSessionId: deleteField() }),
|
|
2451
|
+
},
|
|
2452
|
+
{
|
|
2453
|
+
path: "ctrl/activeSessionId",
|
|
2454
|
+
promise: remove(ref(rtdb, `events/${eventId}/ctrl/activeSessionId`)),
|
|
2455
|
+
},
|
|
2409
2456
|
];
|
|
2410
|
-
if (sessionId)
|
|
2411
|
-
cleanup.push(
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
.
|
|
2418
|
-
|
|
2457
|
+
if (sessionId) {
|
|
2458
|
+
cleanup.push({
|
|
2459
|
+
path: `activeSessions/${sessionId}`,
|
|
2460
|
+
promise: remove(ref(rtdb, `activeSessions/${sessionId}`)),
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
if (sessionId && deleteRtdbSessionData) {
|
|
2464
|
+
cleanup.push({
|
|
2465
|
+
path: `events/${eventId}/sessions/${sessionId}`,
|
|
2466
|
+
promise: remove(ref(rtdb, `events/${eventId}/sessions/${sessionId}`)),
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
const settled = await Promise.allSettled(cleanup.map((operation) => operation.promise));
|
|
2470
|
+
const attempts = settled.map((result, index) => {
|
|
2471
|
+
const operation = cleanup[index];
|
|
2472
|
+
if (!operation)
|
|
2473
|
+
throw new Error("Session repair result has no matching cleanup operation.");
|
|
2474
|
+
return {
|
|
2475
|
+
path: operation.path,
|
|
2476
|
+
status: result.status,
|
|
2477
|
+
...(result.status === "rejected" ? {
|
|
2478
|
+
error: result.reason instanceof Error ? result.reason.message : String(result.reason),
|
|
2479
|
+
} : {}),
|
|
2480
|
+
};
|
|
2481
|
+
});
|
|
2482
|
+
let firestorePointer = null;
|
|
2483
|
+
let rtdbPointer = null;
|
|
2484
|
+
let verificationError;
|
|
2485
|
+
try {
|
|
2486
|
+
const [eventAfter, ctrlAfter] = await Promise.all([
|
|
2487
|
+
getDoc(doc(firestore, "events", eventId)),
|
|
2488
|
+
get(ref(rtdb, `events/${eventId}/ctrl/activeSessionId`)),
|
|
2489
|
+
]);
|
|
2490
|
+
firestorePointer = eventAfter.exists()
|
|
2491
|
+
? safeString(eventAfter.data().activeAnalyticsSessionId) || null
|
|
2492
|
+
: null;
|
|
2493
|
+
rtdbPointer = safeString(ctrlAfter.val()) || null;
|
|
2494
|
+
}
|
|
2495
|
+
catch (error) {
|
|
2496
|
+
verificationError = error instanceof Error ? error.message : String(error);
|
|
2497
|
+
}
|
|
2498
|
+
return textResult(summarizeSessionRepair({
|
|
2419
2499
|
eventId,
|
|
2420
2500
|
sessionId: sessionId || null,
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2501
|
+
deleteRtdbSessionData,
|
|
2502
|
+
attempts,
|
|
2503
|
+
firestorePointer,
|
|
2504
|
+
rtdbPointer,
|
|
2505
|
+
verificationError,
|
|
2506
|
+
}));
|
|
2425
2507
|
});
|
|
2426
2508
|
server.registerTool("oko_delete_session", {
|
|
2427
2509
|
title: "Delete analytics session",
|
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 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
// Generated by scripts/generateMcpVersion.mjs from mcp/package.json. Do not edit.
|
|
2
|
+
export const OKO_MCP_VERSION = "0.9.3";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@okostream/mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Control OKO Stream webinars from Claude, Codex, and other MCP clients.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"oko-mcp": "dist/server.js"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
|
-
"prebuild": "node ../scripts/generateMcpContract.mjs --check",
|
|
34
|
+
"prebuild": "node ../scripts/generateMcpVersion.mjs && node ../scripts/generateMcpContract.mjs --check",
|
|
35
35
|
"build": "tsc && node -e \"require('node:fs').chmodSync('dist/server.js', 0o755)\"",
|
|
36
36
|
"test": "npm run build && node --test test/*.test.mjs",
|
|
37
37
|
"smoke:tarball": "node test/tarball-smoke.mjs",
|