@okostream/mcp 0.1.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.
- package/README.md +143 -0
- package/dist/auth.js +48 -0
- package/dist/env.js +35 -0
- package/dist/firebase.js +22 -0
- package/dist/firebaseDefaults.js +11 -0
- package/dist/scopes.js +22 -0
- package/dist/server.js +29 -0
- package/dist/tools.js +1556 -0
- package/dist/utils.js +79 -0
- package/dist/videoUrl.js +162 -0
- package/package.json +41 -0
package/dist/utils.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const DEFAULT_CHARACTER_LIMIT = 30_000;
|
|
2
|
+
const MAX_COMPACT_ARRAY_ITEMS = 25;
|
|
3
|
+
const MAX_COMPACT_OBJECT_KEYS = 60;
|
|
4
|
+
const MAX_COMPACT_STRING_LENGTH = 2_000;
|
|
5
|
+
export function serialize(value) {
|
|
6
|
+
if (value == null)
|
|
7
|
+
return value;
|
|
8
|
+
if (typeof value !== "object")
|
|
9
|
+
return value;
|
|
10
|
+
if (typeof value.toDate === "function") {
|
|
11
|
+
return value.toDate().toISOString();
|
|
12
|
+
}
|
|
13
|
+
if (Array.isArray(value))
|
|
14
|
+
return value.map(serialize);
|
|
15
|
+
const out = {};
|
|
16
|
+
for (const [key, item] of Object.entries(value)) {
|
|
17
|
+
if (item !== undefined)
|
|
18
|
+
out[key] = serialize(item);
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
export function docJson(snap) {
|
|
23
|
+
return { id: snap.id, ...serialize(snap.data()) };
|
|
24
|
+
}
|
|
25
|
+
function compactForText(value, depth = 0) {
|
|
26
|
+
if (typeof value === "string" && value.length > MAX_COMPACT_STRING_LENGTH) {
|
|
27
|
+
return `${value.slice(0, MAX_COMPACT_STRING_LENGTH)}... [truncated ${value.length - MAX_COMPACT_STRING_LENGTH} chars]`;
|
|
28
|
+
}
|
|
29
|
+
if (value == null || typeof value !== "object")
|
|
30
|
+
return value;
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
const items = value.slice(0, MAX_COMPACT_ARRAY_ITEMS).map((item) => compactForText(item, depth + 1));
|
|
33
|
+
if (value.length > MAX_COMPACT_ARRAY_ITEMS) {
|
|
34
|
+
items.push({ omittedItems: value.length - MAX_COMPACT_ARRAY_ITEMS });
|
|
35
|
+
}
|
|
36
|
+
return items;
|
|
37
|
+
}
|
|
38
|
+
if (depth >= 4)
|
|
39
|
+
return "[nested object omitted]";
|
|
40
|
+
const entries = Object.entries(value);
|
|
41
|
+
const out = {};
|
|
42
|
+
for (const [key, item] of entries.slice(0, MAX_COMPACT_OBJECT_KEYS)) {
|
|
43
|
+
out[key] = compactForText(item, depth + 1);
|
|
44
|
+
}
|
|
45
|
+
if (entries.length > MAX_COMPACT_OBJECT_KEYS) {
|
|
46
|
+
out.omittedKeys = entries.length - MAX_COMPACT_OBJECT_KEYS;
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
export function textResult(data, options = {}) {
|
|
51
|
+
const maxChars = options.maxChars ?? DEFAULT_CHARACTER_LIMIT;
|
|
52
|
+
let structuredContent = serialize(data);
|
|
53
|
+
let text = JSON.stringify(structuredContent, null, 2);
|
|
54
|
+
if (text.length > maxChars) {
|
|
55
|
+
structuredContent = {
|
|
56
|
+
...compactForText(structuredContent),
|
|
57
|
+
truncated: true,
|
|
58
|
+
truncationMessage: `Response JSON exceeded ${maxChars} characters. ${options.truncationHint ?? "Use limit/cursor or narrower include options."}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }],
|
|
63
|
+
structuredContent,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function requireConfirm(confirm, action, preview) {
|
|
67
|
+
if (confirm === true)
|
|
68
|
+
return;
|
|
69
|
+
throw new Error(`${action} requires confirm:true. Preview: ${JSON.stringify(serialize(preview))}`);
|
|
70
|
+
}
|
|
71
|
+
export function csvCell(value) {
|
|
72
|
+
let text = value == null ? "" : String(value);
|
|
73
|
+
if (/^[=+\-@]/.test(text))
|
|
74
|
+
text = `'${text}`;
|
|
75
|
+
return `"${text.replace(/"/g, '""')}"`;
|
|
76
|
+
}
|
|
77
|
+
export function safeString(value, fallback = "") {
|
|
78
|
+
return typeof value === "string" ? value : fallback;
|
|
79
|
+
}
|
package/dist/videoUrl.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export function extractIframeSrc(input) {
|
|
2
|
+
const trimmed = input.trim();
|
|
3
|
+
if (!trimmed.startsWith("<iframe"))
|
|
4
|
+
return null;
|
|
5
|
+
const match = trimmed.match(/\bsrc=["']([^"']+)["']/i);
|
|
6
|
+
return match?.[1] ?? null;
|
|
7
|
+
}
|
|
8
|
+
const YT_ID_RE = /^[\w-]{11}$/;
|
|
9
|
+
function extractYouTubeId(url) {
|
|
10
|
+
let parsed;
|
|
11
|
+
try {
|
|
12
|
+
parsed = new URL(url);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const host = parsed.hostname.replace(/^www\./, "");
|
|
18
|
+
if (host === "youtu.be") {
|
|
19
|
+
const id = parsed.pathname.slice(1).split("/")[0] ?? "";
|
|
20
|
+
return YT_ID_RE.test(id) ? id : null;
|
|
21
|
+
}
|
|
22
|
+
if (host !== "youtube.com" && host !== "m.youtube.com")
|
|
23
|
+
return null;
|
|
24
|
+
const path = parsed.pathname;
|
|
25
|
+
if (path.startsWith("/embed/")) {
|
|
26
|
+
const id = path.slice(7).split("/")[0] ?? "";
|
|
27
|
+
return YT_ID_RE.test(id) ? id : null;
|
|
28
|
+
}
|
|
29
|
+
if (path.startsWith("/shorts/")) {
|
|
30
|
+
const id = path.slice(8).split("/")[0] ?? "";
|
|
31
|
+
return YT_ID_RE.test(id) ? id : null;
|
|
32
|
+
}
|
|
33
|
+
if (path.startsWith("/live/")) {
|
|
34
|
+
const id = path.slice(6).split("/")[0] ?? "";
|
|
35
|
+
return YT_ID_RE.test(id) ? id : null;
|
|
36
|
+
}
|
|
37
|
+
if (path === "/watch") {
|
|
38
|
+
const id = parsed.searchParams.get("v") ?? "";
|
|
39
|
+
return YT_ID_RE.test(id) ? id : null;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function hostOf(url) {
|
|
44
|
+
try {
|
|
45
|
+
return new URL(url).hostname.replace(/^www\./, "").replace(/^m\./, "");
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function extractVkVideoIds(url) {
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = new URL(url);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (parsed.pathname.includes("video_ext.php")) {
|
|
60
|
+
const oid = parsed.searchParams.get("oid");
|
|
61
|
+
const id = parsed.searchParams.get("id");
|
|
62
|
+
return oid && id ? { oid, id } : null;
|
|
63
|
+
}
|
|
64
|
+
const match = parsed.pathname.match(/\/video(-?\d+)_(\d+)/);
|
|
65
|
+
return match ? { oid: match[1] ?? "", id: match[2] ?? "" } : null;
|
|
66
|
+
}
|
|
67
|
+
function extractPathId(url, host, patterns) {
|
|
68
|
+
let parsed;
|
|
69
|
+
try {
|
|
70
|
+
parsed = new URL(url);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (parsed.hostname.replace(/^www\./, "") !== host && !parsed.hostname.endsWith(`.${host}`))
|
|
76
|
+
return null;
|
|
77
|
+
for (const pattern of patterns) {
|
|
78
|
+
const match = parsed.pathname.match(pattern);
|
|
79
|
+
if (match?.[1])
|
|
80
|
+
return match[1];
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
function extractTwitchEmbed(url) {
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = new URL(url);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const host = parsed.hostname.replace(/^www\./, "");
|
|
93
|
+
if (host === "player.twitch.tv") {
|
|
94
|
+
const channel = parsed.searchParams.get("channel");
|
|
95
|
+
const video = parsed.searchParams.get("video");
|
|
96
|
+
if (channel)
|
|
97
|
+
return `https://player.twitch.tv/?channel=${channel}&parent=okostream.app`;
|
|
98
|
+
if (video)
|
|
99
|
+
return `https://player.twitch.tv/?video=${video}&parent=okostream.app`;
|
|
100
|
+
}
|
|
101
|
+
if (host === "twitch.tv") {
|
|
102
|
+
const vodMatch = parsed.pathname.match(/\/videos\/(\d+)/);
|
|
103
|
+
if (vodMatch?.[1])
|
|
104
|
+
return `https://player.twitch.tv/?video=v${vodMatch[1]}&parent=okostream.app`;
|
|
105
|
+
const channelMatch = parsed.pathname.match(/^\/([a-zA-Z0-9_]+)$/);
|
|
106
|
+
if (channelMatch?.[1])
|
|
107
|
+
return `https://player.twitch.tv/?channel=${channelMatch[1]}&parent=okostream.app`;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
export function detectAndNormalize(url) {
|
|
112
|
+
const trimmed = url.trim();
|
|
113
|
+
if (!trimmed)
|
|
114
|
+
return { provider: "unknown", normalizedUrl: null };
|
|
115
|
+
const host = hostOf(trimmed);
|
|
116
|
+
if (host === "youtube.com" || host === "youtu.be") {
|
|
117
|
+
const id = extractYouTubeId(trimmed);
|
|
118
|
+
return { provider: "youtube", normalizedUrl: id ? `https://www.youtube.com/embed/${id}` : null };
|
|
119
|
+
}
|
|
120
|
+
if (host === "vk.com" || host === "vkvideo.ru") {
|
|
121
|
+
const ids = extractVkVideoIds(trimmed);
|
|
122
|
+
return { provider: "vk", normalizedUrl: ids ? `https://vk.com/video_ext.php?oid=${ids.oid}&id=${ids.id}&hd=2` : null };
|
|
123
|
+
}
|
|
124
|
+
const rutubeId = extractPathId(trimmed, "rutube.ru", [/\/play\/embed\/([a-f0-9]+)/i, /\/video\/([a-f0-9]+)/i]);
|
|
125
|
+
if (rutubeId)
|
|
126
|
+
return { provider: "rutube", normalizedUrl: `https://rutube.ru/play/embed/${rutubeId}` };
|
|
127
|
+
if (trimmed.includes("rutube.ru"))
|
|
128
|
+
return { provider: "rutube", normalizedUrl: null };
|
|
129
|
+
const vimeoId = extractPathId(trimmed, "vimeo.com", [/\/video\/(\d+)/, /^\/(\d+)/]);
|
|
130
|
+
if (vimeoId)
|
|
131
|
+
return { provider: "vimeo", normalizedUrl: `https://player.vimeo.com/video/${vimeoId}` };
|
|
132
|
+
if (trimmed.includes("vimeo.com"))
|
|
133
|
+
return { provider: "vimeo", normalizedUrl: null };
|
|
134
|
+
const kinescopeId = extractPathId(trimmed, "kinescope.io", [/\/embed\/([a-zA-Z0-9]+)/, /^\/([a-zA-Z0-9]+)$/]);
|
|
135
|
+
if (kinescopeId)
|
|
136
|
+
return { provider: "kinescope", normalizedUrl: `https://kinescope.io/embed/${kinescopeId}` };
|
|
137
|
+
if (trimmed.includes("kinescope.io"))
|
|
138
|
+
return { provider: "kinescope", normalizedUrl: null };
|
|
139
|
+
const dzenId = extractPathId(trimmed, "dzen.ru", [/\/embed\/([a-zA-Z0-9_-]+)/, /\/video\/watch\/([a-zA-Z0-9_-]+)/]);
|
|
140
|
+
if (dzenId)
|
|
141
|
+
return { provider: "dzen", normalizedUrl: `https://dzen.ru/embed/${dzenId}` };
|
|
142
|
+
if (trimmed.includes("dzen.ru"))
|
|
143
|
+
return { provider: "dzen", normalizedUrl: null };
|
|
144
|
+
const twitchEmbed = extractTwitchEmbed(trimmed);
|
|
145
|
+
if (twitchEmbed)
|
|
146
|
+
return { provider: "twitch", normalizedUrl: twitchEmbed };
|
|
147
|
+
if (trimmed.includes("twitch.tv"))
|
|
148
|
+
return { provider: "twitch", normalizedUrl: null };
|
|
149
|
+
const dailymotionId = extractPathId(trimmed, "dailymotion.com", [/\/embed\/video\/([a-zA-Z0-9]+)/, /\/video\/([a-zA-Z0-9]+)/])
|
|
150
|
+
?? extractPathId(trimmed, "dai.ly", [/^\/([a-zA-Z0-9]+)/]);
|
|
151
|
+
if (dailymotionId)
|
|
152
|
+
return { provider: "dailymotion", normalizedUrl: `https://www.dailymotion.com/embed/video/${dailymotionId}` };
|
|
153
|
+
if (trimmed.includes("dailymotion.com") || trimmed.includes("dai.ly"))
|
|
154
|
+
return { provider: "dailymotion", normalizedUrl: null };
|
|
155
|
+
const wistiaId = extractPathId(trimmed, "wistia.com", [/\/medias\/([a-zA-Z0-9]+)/])
|
|
156
|
+
?? extractPathId(trimmed, "fast.wistia.net", [/\/embed\/iframe\/([a-zA-Z0-9]+)/]);
|
|
157
|
+
if (wistiaId)
|
|
158
|
+
return { provider: "wistia", normalizedUrl: `https://fast.wistia.net/embed/iframe/${wistiaId}` };
|
|
159
|
+
if (trimmed.includes("wistia.com") || trimmed.includes("wistia.net"))
|
|
160
|
+
return { provider: "wistia", normalizedUrl: null };
|
|
161
|
+
return { provider: "unknown", normalizedUrl: null };
|
|
162
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@okostream/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for managing OKO Stream webinars as an organizer.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/server.js",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/BelfanioXL/okostreamcore.git",
|
|
11
|
+
"directory": "mcp"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"oko-mcp": "dist/server.js"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"prepublishOnly": "npm run build",
|
|
26
|
+
"start": "node dist/server.js"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
33
|
+
"dotenv": "^17.2.3",
|
|
34
|
+
"firebase": "^12.10.0",
|
|
35
|
+
"zod": "^3.25.76"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.19.37",
|
|
39
|
+
"typescript": "^5.9.3"
|
|
40
|
+
}
|
|
41
|
+
}
|