@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/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# OKO MCP Server
|
|
2
|
+
|
|
3
|
+
MCP server for managing OKO Stream webinars as an organizer.
|
|
4
|
+
|
|
5
|
+
## Quick Setup
|
|
6
|
+
|
|
7
|
+
1. Create an API key in OKO: `/app/settings` -> API keys for MCP.
|
|
8
|
+
2. Add the server to your MCP client config.
|
|
9
|
+
|
|
10
|
+
Claude Desktop / Claude Code:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"mcpServers": {
|
|
15
|
+
"oko": {
|
|
16
|
+
"command": "npx",
|
|
17
|
+
"args": ["-y", "@okostream/mcp"],
|
|
18
|
+
"env": {
|
|
19
|
+
"OKO_API_KEY": "oko_sk_..."
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Codex:
|
|
27
|
+
|
|
28
|
+
```toml
|
|
29
|
+
[mcp_servers.oko]
|
|
30
|
+
command = "npx"
|
|
31
|
+
args = ["-y", "@okostream/mcp"]
|
|
32
|
+
|
|
33
|
+
[mcp_servers.oko.env]
|
|
34
|
+
OKO_API_KEY = "oko_sk_..."
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The package includes the public Firebase config for `okostreamcore`, so `OKO_API_KEY` is the only required variable. For staging or local experiments, override these optional variables:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
OKO_EXCHANGE_URL=...
|
|
41
|
+
OKO_FIREBASE_API_KEY=...
|
|
42
|
+
OKO_FIREBASE_AUTH_DOMAIN=...
|
|
43
|
+
OKO_FIREBASE_DATABASE_URL=...
|
|
44
|
+
OKO_FIREBASE_CHAT_DB_URL=...
|
|
45
|
+
OKO_FIREBASE_PROJECT_ID=...
|
|
46
|
+
OKO_FIREBASE_STORAGE_BUCKET=...
|
|
47
|
+
OKO_FIREBASE_MESSAGING_SENDER_ID=...
|
|
48
|
+
OKO_FIREBASE_APP_ID=...
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Response Contract
|
|
52
|
+
|
|
53
|
+
Every tool returns JSON text plus MCP `structuredContent`. List-like tools use bounded pages:
|
|
54
|
+
|
|
55
|
+
- `limit` defaults to 20-50 depending on the tool.
|
|
56
|
+
- Cursor tools return both `hasMore`/`nextCursor` and `has_more`/`next_cursor`.
|
|
57
|
+
- `oko_list_my_events`, `oko_get_analytics`, `oko_list_recent_chat`, `oko_list_participants`, and `oko_list_leads` support pagination.
|
|
58
|
+
- `oko_get_analytics` omits heavy `timeline`, baseline/final snapshots, and NMO participant rows by default. Request `includeTimeline`, `includeSnapshots`, or `includeNmoRows` only when needed.
|
|
59
|
+
- Oversized responses are truncated with `truncated: true` and a `truncationMessage`.
|
|
60
|
+
|
|
61
|
+
This package is published as source-available runtime distribution for OKO customers. `license: UNLICENSED` means all rights reserved unless a separate agreement says otherwise.
|
|
62
|
+
|
|
63
|
+
## Local Development
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cd mcp
|
|
67
|
+
npm install
|
|
68
|
+
npm run build
|
|
69
|
+
npm start
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Local config without npm publishing:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"oko": {
|
|
78
|
+
"command": "node",
|
|
79
|
+
"args": ["/absolute/path/to/okostream/mcp/dist/server.js"],
|
|
80
|
+
"env": {
|
|
81
|
+
"OKO_API_KEY": "oko_sk_..."
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Tools
|
|
89
|
+
|
|
90
|
+
All tools are prefixed with `oko_` for safe discovery next to other MCP servers.
|
|
91
|
+
|
|
92
|
+
Read tools include:
|
|
93
|
+
|
|
94
|
+
- `oko_whoami`
|
|
95
|
+
- `oko_list_workspaces`
|
|
96
|
+
- `oko_list_my_events`
|
|
97
|
+
- `oko_get_event`
|
|
98
|
+
- `oko_get_live_state`
|
|
99
|
+
- `oko_get_analytics`
|
|
100
|
+
- `oko_list_recent_chat`
|
|
101
|
+
- `oko_list_participants`
|
|
102
|
+
- `oko_list_leads`
|
|
103
|
+
|
|
104
|
+
Write/control tools include:
|
|
105
|
+
|
|
106
|
+
- `oko_create_webinar`
|
|
107
|
+
- `oko_update_event`
|
|
108
|
+
- `oko_configure_blocks`
|
|
109
|
+
- `oko_set_block_visibility`
|
|
110
|
+
- `oko_refresh_viewers`
|
|
111
|
+
- `oko_start_session`
|
|
112
|
+
- `oko_end_session`
|
|
113
|
+
- `oko_send_chat_message`
|
|
114
|
+
- `oko_delete_chat_message`
|
|
115
|
+
- `oko_mute_user`
|
|
116
|
+
- `oko_ban_user`
|
|
117
|
+
- `oko_set_chat_settings`
|
|
118
|
+
- `oko_launch_checkpoint`
|
|
119
|
+
- `oko_close_checkpoint`
|
|
120
|
+
- `oko_publish_telegram_post`
|
|
121
|
+
- `oko_create_workspace_invite`
|
|
122
|
+
- `oko_delete_event`
|
|
123
|
+
|
|
124
|
+
Dangerous or external side-effect tools require `confirm: true`.
|
|
125
|
+
|
|
126
|
+
## Smoke
|
|
127
|
+
|
|
128
|
+
1. `oko_whoami` should return the organizer UID and granted scopes.
|
|
129
|
+
2. `oko_list_my_events` should return only events visible to that UID.
|
|
130
|
+
3. `oko_create_webinar` should create a test webinar and RTDB owner/workspace mirror.
|
|
131
|
+
4. `oko_get_analytics` should read an existing event with analytics sessions.
|
|
132
|
+
|
|
133
|
+
CLI help:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npx -y @okostream/mcp --help
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Troubleshooting
|
|
140
|
+
|
|
141
|
+
If Claude shows `Unexpected token '◇'`, the MCP server printed a non-JSON line to stdout. Use the current version with `dotenv quiet:true`, then fully restart the MCP client.
|
|
142
|
+
|
|
143
|
+
For stdio MCP, stdout must contain only JSON-RPC. Diagnostics belong on stderr.
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { signInWithCustomToken } from "firebase/auth";
|
|
2
|
+
import { auth } from "./firebase.js";
|
|
3
|
+
import { env } from "./env.js";
|
|
4
|
+
import { sanitizeScopes } from "./scopes.js";
|
|
5
|
+
let state = null;
|
|
6
|
+
async function exchangeApiKey() {
|
|
7
|
+
const response = await fetch(env.exchangeUrl, {
|
|
8
|
+
method: "POST",
|
|
9
|
+
headers: { "content-type": "application/json" },
|
|
10
|
+
body: JSON.stringify({ apiKey: env.apiKey }),
|
|
11
|
+
});
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
const status = response.status;
|
|
14
|
+
throw new Error(status === 401
|
|
15
|
+
? "OKO API key is invalid or revoked. Create a fresh key in OKO Settings and set OKO_API_KEY."
|
|
16
|
+
: `OKO API key exchange failed with HTTP ${status}. Check OKO_EXCHANGE_URL and try again.`);
|
|
17
|
+
}
|
|
18
|
+
const data = await response.json();
|
|
19
|
+
if (typeof data.customToken !== "string") {
|
|
20
|
+
throw new Error("OKO API key exchange returned no customToken.");
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
customToken: data.customToken,
|
|
24
|
+
expiresIn: typeof data.expiresIn === "number" ? data.expiresIn : 3600,
|
|
25
|
+
scopes: sanitizeScopes(data.scopes),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export async function ensureAuth(requiredScopes = []) {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
if (!state || now > state.expiresAt - 5 * 60 * 1000 || !auth.currentUser) {
|
|
31
|
+
const exchanged = await exchangeApiKey();
|
|
32
|
+
const credential = await signInWithCustomToken(auth, exchanged.customToken);
|
|
33
|
+
state = {
|
|
34
|
+
uid: credential.user.uid,
|
|
35
|
+
scopes: exchanged.scopes,
|
|
36
|
+
expiresAt: now + exchanged.expiresIn * 1000,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
for (const scope of requiredScopes) {
|
|
40
|
+
if (!state.scopes.includes(scope)) {
|
|
41
|
+
throw new Error(`API key scope required: ${scope}. Create or choose an OKO API key with that scope.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return state;
|
|
45
|
+
}
|
|
46
|
+
export function currentScopes() {
|
|
47
|
+
return state?.scopes ?? [];
|
|
48
|
+
}
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { config } from "dotenv";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { DEFAULT_EXCHANGE_URL, DEFAULT_FIREBASE_CONFIG } from "./firebaseDefaults.js";
|
|
5
|
+
config({ path: join(homedir(), ".env"), quiet: true });
|
|
6
|
+
function readEnv(...names) {
|
|
7
|
+
for (const name of names) {
|
|
8
|
+
const value = process.env[name]?.trim();
|
|
9
|
+
if (value)
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
throw new Error(`Missing required env var: ${names.join(" or ")}`);
|
|
13
|
+
}
|
|
14
|
+
function readEnvWithDefault(defaultValue, ...names) {
|
|
15
|
+
for (const name of names) {
|
|
16
|
+
const value = process.env[name]?.trim();
|
|
17
|
+
if (value)
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
return defaultValue;
|
|
21
|
+
}
|
|
22
|
+
export const env = {
|
|
23
|
+
apiKey: readEnv("OKO_API_KEY"),
|
|
24
|
+
exchangeUrl: readEnvWithDefault(DEFAULT_EXCHANGE_URL, "OKO_EXCHANGE_URL"),
|
|
25
|
+
firebase: {
|
|
26
|
+
apiKey: readEnvWithDefault(DEFAULT_FIREBASE_CONFIG.apiKey, "OKO_FIREBASE_API_KEY", "NEXT_PUBLIC_FIREBASE_API_KEY"),
|
|
27
|
+
authDomain: readEnvWithDefault(DEFAULT_FIREBASE_CONFIG.authDomain, "OKO_FIREBASE_AUTH_DOMAIN", "NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN"),
|
|
28
|
+
databaseURL: readEnvWithDefault(DEFAULT_FIREBASE_CONFIG.databaseURL, "OKO_FIREBASE_DATABASE_URL", "NEXT_PUBLIC_FIREBASE_DATABASE_URL"),
|
|
29
|
+
projectId: readEnvWithDefault(DEFAULT_FIREBASE_CONFIG.projectId, "OKO_FIREBASE_PROJECT_ID", "NEXT_PUBLIC_FIREBASE_PROJECT_ID"),
|
|
30
|
+
storageBucket: readEnvWithDefault(DEFAULT_FIREBASE_CONFIG.storageBucket, "OKO_FIREBASE_STORAGE_BUCKET", "NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET"),
|
|
31
|
+
messagingSenderId: readEnvWithDefault(DEFAULT_FIREBASE_CONFIG.messagingSenderId, "OKO_FIREBASE_MESSAGING_SENDER_ID", "NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID"),
|
|
32
|
+
appId: readEnvWithDefault(DEFAULT_FIREBASE_CONFIG.appId, "OKO_FIREBASE_APP_ID", "NEXT_PUBLIC_FIREBASE_APP_ID"),
|
|
33
|
+
chatDatabaseURL: readEnvWithDefault(DEFAULT_FIREBASE_CONFIG.chatDatabaseURL, "OKO_FIREBASE_CHAT_DB_URL", "NEXT_PUBLIC_FIREBASE_CHAT_DB_URL"),
|
|
34
|
+
},
|
|
35
|
+
};
|
package/dist/firebase.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { initializeApp, getApp, getApps } from "firebase/app";
|
|
2
|
+
import { getAuth } from "firebase/auth";
|
|
3
|
+
import { getDatabase } from "firebase/database";
|
|
4
|
+
import { getFirestore } from "firebase/firestore";
|
|
5
|
+
import { getFunctions } from "firebase/functions";
|
|
6
|
+
import { env } from "./env.js";
|
|
7
|
+
export const firebaseApp = getApps().length ? getApp() : initializeApp({
|
|
8
|
+
apiKey: env.firebase.apiKey,
|
|
9
|
+
authDomain: env.firebase.authDomain,
|
|
10
|
+
databaseURL: env.firebase.databaseURL,
|
|
11
|
+
projectId: env.firebase.projectId,
|
|
12
|
+
storageBucket: env.firebase.storageBucket,
|
|
13
|
+
messagingSenderId: env.firebase.messagingSenderId,
|
|
14
|
+
appId: env.firebase.appId,
|
|
15
|
+
});
|
|
16
|
+
export const auth = getAuth(firebaseApp);
|
|
17
|
+
export const firestore = getFirestore(firebaseApp);
|
|
18
|
+
export const rtdb = getDatabase(firebaseApp);
|
|
19
|
+
export const chatRtdb = env.firebase.chatDatabaseURL
|
|
20
|
+
? getDatabase(firebaseApp, env.firebase.chatDatabaseURL)
|
|
21
|
+
: getDatabase(firebaseApp);
|
|
22
|
+
export const functions = getFunctions(firebaseApp, "us-central1");
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const DEFAULT_EXCHANGE_URL = "https://us-central1-okostreamcore.cloudfunctions.net/exchangeApiKey";
|
|
2
|
+
export const DEFAULT_FIREBASE_CONFIG = {
|
|
3
|
+
apiKey: "AIzaSyCbin_AY-tG62I-_n0lkpdfPf8SH9XmqAg",
|
|
4
|
+
authDomain: "okostreamcore.firebaseapp.com",
|
|
5
|
+
databaseURL: "https://okostreamcore-default-rtdb.firebaseio.com",
|
|
6
|
+
chatDatabaseURL: "https://okostreamcore-chat.firebaseio.com",
|
|
7
|
+
projectId: "okostreamcore",
|
|
8
|
+
storageBucket: "okostreamcore.firebasestorage.app",
|
|
9
|
+
messagingSenderId: "876016833190",
|
|
10
|
+
appId: "1:876016833190:web:6d12602c6c2c57491b9a67",
|
|
11
|
+
};
|
package/dist/scopes.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const MCP_SCOPES = [
|
|
2
|
+
"events:read",
|
|
3
|
+
"events:write",
|
|
4
|
+
"events:delete",
|
|
5
|
+
"setup:write",
|
|
6
|
+
"analytics:read",
|
|
7
|
+
"analytics:write",
|
|
8
|
+
"live:control",
|
|
9
|
+
"chat:write",
|
|
10
|
+
"chat:moderate",
|
|
11
|
+
"telegram:write",
|
|
12
|
+
"workspace:read",
|
|
13
|
+
"workspace:write",
|
|
14
|
+
"nmo:write",
|
|
15
|
+
"leads:read",
|
|
16
|
+
];
|
|
17
|
+
const SCOPE_SET = new Set(MCP_SCOPES);
|
|
18
|
+
export function sanitizeScopes(scopes) {
|
|
19
|
+
if (!Array.isArray(scopes))
|
|
20
|
+
return [];
|
|
21
|
+
return scopes.filter((scope) => typeof scope === "string" && SCOPE_SET.has(scope));
|
|
22
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { registerTools } from "./tools.js";
|
|
5
|
+
if (process.argv.includes("--help")) {
|
|
6
|
+
process.stdout.write([
|
|
7
|
+
"OKO MCP Server",
|
|
8
|
+
"",
|
|
9
|
+
"Usage:",
|
|
10
|
+
" oko-mcp",
|
|
11
|
+
" npx -y @okostream/mcp",
|
|
12
|
+
"",
|
|
13
|
+
"Required env:",
|
|
14
|
+
" OKO_API_KEY=oko_sk_...",
|
|
15
|
+
"",
|
|
16
|
+
"Optional env overrides:",
|
|
17
|
+
" OKO_EXCHANGE_URL, OKO_FIREBASE_*",
|
|
18
|
+
"",
|
|
19
|
+
"The server speaks MCP over stdio. Do not run it manually before connecting a client.",
|
|
20
|
+
].join("\n") + "\n");
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
const server = new McpServer({
|
|
24
|
+
name: "oko-mcp-server",
|
|
25
|
+
version: "0.1.0",
|
|
26
|
+
});
|
|
27
|
+
registerTools(server);
|
|
28
|
+
const transport = new StdioServerTransport();
|
|
29
|
+
await server.connect(transport);
|