streamby-core 0.36.0 → 0.37.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/CHANGELOG.md +34 -0
- package/README.md +80 -44
- package/dist/adapters/database/connectionManager.js +24 -11
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/mcp/server.d.ts +4 -3
- package/dist/mcp/server.js +13 -10
- package/dist/mcp/tools.d.ts +5 -2
- package/dist/mcp/tools.js +83 -24
- package/dist/middleware/routes/apiToken.js +77 -14
- package/dist/middleware/routes/member.js +8 -0
- package/dist/middleware/routes/project.js +8 -2
- package/dist/middleware/routes/subscription.js +3 -0
- package/dist/services/apiTokenPermissions.d.ts +16 -0
- package/dist/services/apiTokenPermissions.js +98 -0
- package/dist/services/apiTokens.d.ts +38 -14
- package/dist/services/apiTokens.js +134 -40
- package/dist/services/auth.d.ts +4 -1
- package/dist/services/auth.js +24 -18
- package/dist/types/index.d.ts +5 -2
- package/dist/utils/auth.d.ts +12 -1
- package/dist/utils/auth.js +23 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.37.0
|
|
4
|
+
|
|
5
|
+
### ⚠️ Breaking changes
|
|
6
|
+
|
|
7
|
+
- **API tokens have permissions, not an access level.** The tokens made before this version no
|
|
8
|
+
longer work, and aren't listed: make new ones. There's no migration. The `api_tokens` table
|
|
9
|
+
gets its new columns at startup (`kind`, `permissions`, `createdByName`), and its old
|
|
10
|
+
`access` column becomes optional; nothing else is changed.
|
|
11
|
+
- **`ApiTokenScope` changed.** It has `kind` (`'user'` or `'project'`) and `permissions`,
|
|
12
|
+
instead of `access`. `ApiTokenAccess` is gone.
|
|
13
|
+
- **`createApiToken`** takes who makes the token (`{ kind, userId, username, projectId }`), and
|
|
14
|
+
`permissions` instead of `access`. `listApiTokens` and `revokeApiToken` are now
|
|
15
|
+
`listUserApiTokens` and `revokeUserApiToken`.
|
|
16
|
+
- **The host's `authProvider` must handle a project's token** (see the README): it isn't a
|
|
17
|
+
user, so it must not be looked up as one. `resolveApiToken` returns its `name` to go by.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **Permissions** for tokens, by resource and action: `project`, `members`, `assets`,
|
|
22
|
+
`production`, `workflow`, `exports`, `rights` and `jobs`, each with `read` and its writes
|
|
23
|
+
(`update`, `write`, `manage`, `delete` or `run`), and `projects:create` for the account.
|
|
24
|
+
Every route a token reaches needs its own permission; a route no permission covers isn't
|
|
25
|
+
reachable with a token. A user's token is still limited by its user's role in each project.
|
|
26
|
+
`PROJECT_PERMISSIONS` and `ACCOUNT_PERMISSIONS` are exported.
|
|
27
|
+
- **Projects' tokens**, at `GET`, `POST` and `DELETE /projects/:id/api-tokens`, managed by the
|
|
28
|
+
project's admins only. They act on their project alone, with their permissions, and keep
|
|
29
|
+
working after whoever made them leaves it; the project's admins are notified when that
|
|
30
|
+
happens (`api_token_creator_left`). Deleting a project ends its tokens.
|
|
31
|
+
- **`projects:create`**: a user's token with it creates projects, and reaches the ones it
|
|
32
|
+
creates.
|
|
33
|
+
- **MCP tools** `create_project`, `delete_project`, `delete_asset`, `delete_sequence`,
|
|
34
|
+
`delete_shot` and `delete_task`. Each tool needs its permissions and is listed only for a
|
|
35
|
+
token that has them; the ones that delete are marked as destructive.
|
|
36
|
+
|
|
3
37
|
## 0.36.0
|
|
4
38
|
|
|
5
39
|
### Added
|
package/README.md
CHANGED
|
@@ -102,20 +102,49 @@ createStreamByRouter({
|
|
|
102
102
|
|
|
103
103
|
## 🎫 API tokens
|
|
104
104
|
|
|
105
|
-
Agents and services work with StreamBy
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
- **
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
105
|
+
Agents and services work with StreamBy through API tokens, without a browser session. There
|
|
106
|
+
are two kinds:
|
|
107
|
+
|
|
108
|
+
- **A user's token** acts as its user. The user manages their own (`GET`, `POST` and
|
|
109
|
+
`DELETE /streamby/user/api-tokens`). It reaches a list of the user's projects, or `'*'` for
|
|
110
|
+
every project they're a member of, including ones they join later. It never does more than
|
|
111
|
+
its user can: their role in each project still applies.
|
|
112
|
+
- **A project's token** acts on its project alone, as the project's, not as whoever made it:
|
|
113
|
+
it keeps working after they leave the project, for permanent integrations (CI, services, the
|
|
114
|
+
project's agents). Only the project's admins list, make and revoke them (`GET`, `POST` and
|
|
115
|
+
`DELETE /streamby/projects/:id/api-tokens`), and each one records who made it. When they
|
|
116
|
+
leave, the project's admins are notified that it still works. Deleting the project ends
|
|
117
|
+
its tokens.
|
|
118
|
+
|
|
119
|
+
Tokens are managed only with a session: a token can't manage tokens.
|
|
120
|
+
|
|
121
|
+
Each token has a name, **its permissions**, and **an expiry** (7, 30, 90 or 365 days, or
|
|
122
|
+
none). It's `sbt_` followed by 32 random bytes, shown once, when it's made. Only its SHA-256
|
|
123
|
+
hash is stored, with a short prefix to tell it apart.
|
|
124
|
+
|
|
125
|
+
**Permissions**
|
|
126
|
+
|
|
127
|
+
| Resource | Permissions | Covers |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| Project | `project:read`, `project:update`, `project:delete` | The project, its settings and its connections (API, database, storage, distribution, render farm), integrations and allowed origins. |
|
|
130
|
+
| Members | `members:read`, `members:manage` | Listing members; inviting, changing and removing them, and searching users. |
|
|
131
|
+
| Assets | `assets:read`, `assets:write`, `assets:delete` | Files and folders, upload URLs, metadata, versions, reviews and annotations, QC reports. |
|
|
132
|
+
| Production | `production:read`, `production:write`, `production:delete` | Sequences, shots and tasks. |
|
|
133
|
+
| Workflow | `workflow:read`, `workflow:write` | The workflow and its pipelines. |
|
|
134
|
+
| Exports | `exports:read`, `exports:write` | The project's exports. |
|
|
135
|
+
| Rights | `rights:read`, `rights:write` | Assets' rights. |
|
|
136
|
+
| Jobs | `jobs:read`, `jobs:run` | Following jobs; starting them (transcoding, rendering, AI, publishing a deliverable). |
|
|
137
|
+
| Account | `projects:create` | Creating projects. A user's token only; the token reaches what it creates. |
|
|
138
|
+
|
|
139
|
+
`PROJECT_PERMISSIONS` and `ACCOUNT_PERMISSIONS` are exported. A route no permission covers
|
|
140
|
+
(the user's notifications, integrations and tokens, Explore, accepting an invitation) isn't
|
|
141
|
+
reachable with a token.
|
|
142
|
+
|
|
143
|
+
**Resolving a token**
|
|
116
144
|
|
|
117
145
|
Requests send it as `Authorization: Bearer sbt_...`. Your `authProvider` resolves it with
|
|
118
|
-
`resolveApiToken
|
|
146
|
+
`resolveApiToken`, and returns its `userId` with the token's scope in `apiToken`. A project's
|
|
147
|
+
token isn't a user: its `userId` is the token's own (on its project), and it goes by its name.
|
|
119
148
|
|
|
120
149
|
```ts
|
|
121
150
|
import { createStreamByRouter, resolveApiToken, API_TOKEN_PREFIX } from 'streamby-core';
|
|
@@ -127,6 +156,9 @@ createStreamByRouter({
|
|
|
127
156
|
if (bearer?.startsWith(API_TOKEN_PREFIX)) {
|
|
128
157
|
const resolved = await resolveApiToken(bearer); // null once revoked or expired
|
|
129
158
|
if (!resolved) throw new Error('Invalid API token');
|
|
159
|
+
if (resolved.scope.kind === 'project') {
|
|
160
|
+
return { userId: resolved.userId, username: resolved.name, role: 'viewer', apiToken: resolved.scope };
|
|
161
|
+
}
|
|
130
162
|
const user = await findUser(resolved.userId); // your own user lookup
|
|
131
163
|
return { userId: user.id, username: user.username, role: user.role, apiToken: resolved.scope };
|
|
132
164
|
}
|
|
@@ -135,17 +167,9 @@ createStreamByRouter({
|
|
|
135
167
|
});
|
|
136
168
|
```
|
|
137
169
|
|
|
138
|
-
A request made with a token
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
- a project outside the token's projects answers 404, and the project list shows only the
|
|
142
|
-
token's projects;
|
|
143
|
-
- a token can't manage tokens;
|
|
144
|
-
- outside a project, a token can only make a few reads (`/auth`, `/projects`, `/jobs/:jobId`,
|
|
145
|
-
`/user/subscription`, `/users/search`).
|
|
146
|
-
|
|
147
|
-
The owner's role in each project still applies on top of that. A token never does more than
|
|
148
|
-
its owner can, and it stops reaching a project as soon as its owner leaves it.
|
|
170
|
+
A request made with a token goes through only when its route's permission is among the
|
|
171
|
+
token's (otherwise 403), and a project outside the token's projects answers 404; the project
|
|
172
|
+
list shows only the token's projects.
|
|
149
173
|
|
|
150
174
|
---
|
|
151
175
|
|
|
@@ -156,24 +180,34 @@ StreamBy serves an [MCP](https://modelcontextprotocol.io) server at `POST /strea
|
|
|
156
180
|
projects with an API token (see above). It takes only an API token; a browser session gets 401.
|
|
157
181
|
|
|
158
182
|
Each tool calls StreamBy's own REST API with the caller's token. So it reaches exactly what the
|
|
159
|
-
token's
|
|
160
|
-
made from the UI, which shows it when it loads the project.
|
|
183
|
+
token allows (and, for a user's token, what its user's role in each project allows), and a
|
|
184
|
+
change is saved just like one made from the UI, which shows it when it loads the project.
|
|
161
185
|
|
|
162
186
|
**Tools**
|
|
163
187
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
188
|
+
A token sees only the tools whose permissions it has.
|
|
189
|
+
|
|
190
|
+
| Tools | Permissions |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `list_projects` | any token |
|
|
193
|
+
| `get_project` | `project:read` |
|
|
194
|
+
| `create_project` | `projects:create` |
|
|
195
|
+
| `delete_project` | `project:delete` |
|
|
196
|
+
| `list_assets` (by category, optionally in one storage connection, and filtered by name), `get_asset_metadata` | `assets:read` |
|
|
197
|
+
| `update_asset_metadata` | `assets:write` |
|
|
198
|
+
| `delete_asset` | `assets:delete` |
|
|
199
|
+
| `get_asset_rights` / `set_asset_rights` | `rights:read` / `rights:write` |
|
|
200
|
+
| `get_workflow`, `list_pipelines`, `get_pipeline` | `workflow:read` |
|
|
201
|
+
| `create_pipeline`, `update_pipeline` | `workflow:write` |
|
|
202
|
+
| `list_exports` | `project:read` and `exports:read` |
|
|
203
|
+
| `get_export` | `exports:read` |
|
|
204
|
+
| `get_job` | `jobs:read` |
|
|
205
|
+
| `list_sequences`, `list_shots`, `list_tasks` | `production:read` |
|
|
206
|
+
| `create_sequence`, `update_sequence`, `create_shot`, `update_shot`, `create_task`, `update_task` | `production:write` |
|
|
207
|
+
| `delete_sequence`, `delete_shot`, `delete_task` | `production:delete` |
|
|
208
|
+
|
|
209
|
+
The tools that delete are marked as destructive (`destructiveHint`), so a client can ask before
|
|
210
|
+
running them.
|
|
177
211
|
|
|
178
212
|
**Limits and audit**
|
|
179
213
|
|
|
@@ -181,8 +215,9 @@ made from the UI, which shows it when it loads the project.
|
|
|
181
215
|
answers 429 with `Retry-After`. Each instance counts its own requests. Change the limit with
|
|
182
216
|
`mcp.rateLimit`.
|
|
183
217
|
- Every tool call is recorded in the `mcp_audit` table, created at startup in the main database.
|
|
184
|
-
Each row has the user
|
|
185
|
-
status, the error and the duration. A row never includes
|
|
218
|
+
Each row has the user (a project's token: the token itself), the token's id, the project, the
|
|
219
|
+
tool, whether it went through, the status, the error and the duration. A row never includes
|
|
220
|
+
the arguments or the token.
|
|
186
221
|
|
|
187
222
|
```ts
|
|
188
223
|
createStreamByRouter({
|
|
@@ -197,8 +232,9 @@ createStreamByRouter({
|
|
|
197
232
|
|
|
198
233
|
### Connecting from Claude Code
|
|
199
234
|
|
|
200
|
-
Make an API token in StreamBy
|
|
201
|
-
|
|
235
|
+
Make an API token in StreamBy, with only the permissions the agent needs: your own
|
|
236
|
+
(Settings → Security → Personal API tokens), or a project's (in the project, Settings → API
|
|
237
|
+
Tokens). Keep it in an environment variable, never in a file:
|
|
202
238
|
|
|
203
239
|
```bash
|
|
204
240
|
export STREAMBY_TOKEN=sbt_...
|
|
@@ -223,8 +259,8 @@ environment:
|
|
|
223
259
|
|
|
224
260
|
Claude Desktop runs local (stdio) servers from `claude_desktop_config.json`. Reach StreamBy
|
|
225
261
|
through [`mcp-remote`](https://www.npmjs.com/package/mcp-remote), passing the token in its
|
|
226
|
-
`env`. This file holds it in plain text, so use a token with only the
|
|
227
|
-
an expiry:
|
|
262
|
+
`env`. This file holds it in plain text, so use a token with only the permissions you need,
|
|
263
|
+
and an expiry:
|
|
228
264
|
|
|
229
265
|
```json
|
|
230
266
|
{
|
|
@@ -149,21 +149,34 @@ const ensureTablesExist = async (pool) => {
|
|
|
149
149
|
`);
|
|
150
150
|
console.log('✅ "user_integrations" table ensured to exist.');
|
|
151
151
|
// API tokens: only their hash is kept (see services/apiTokens). Main db only, like
|
|
152
|
-
// user_integrations above.
|
|
152
|
+
// user_integrations above. A table from before tokens had a kind and permissions gets the
|
|
153
|
+
// new columns; its old tokens no longer work (they have no kind) and aren't listed.
|
|
153
154
|
await pool.query(`
|
|
154
155
|
CREATE TABLE IF NOT EXISTS streamby.api_tokens (
|
|
155
|
-
id
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
"
|
|
164
|
-
"
|
|
156
|
+
id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
|
|
157
|
+
kind VARCHAR(10),
|
|
158
|
+
"userId" VARCHAR(255) NOT NULL,
|
|
159
|
+
"createdByName" VARCHAR(255),
|
|
160
|
+
name VARCHAR(255) NOT NULL,
|
|
161
|
+
prefix VARCHAR(32) NOT NULL,
|
|
162
|
+
hash CHAR(64) NOT NULL UNIQUE,
|
|
163
|
+
permissions JSONB NOT NULL DEFAULT '[]',
|
|
164
|
+
"projectIds" JSONB NOT NULL,
|
|
165
|
+
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
166
|
+
"expiresAt" TIMESTAMP WITH TIME ZONE,
|
|
167
|
+
"lastUsedAt" TIMESTAMP WITH TIME ZONE
|
|
165
168
|
);
|
|
169
|
+
ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS kind VARCHAR(10);
|
|
170
|
+
ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS "createdByName" VARCHAR(255);
|
|
171
|
+
ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS permissions JSONB NOT NULL DEFAULT '[]';
|
|
172
|
+
DO $$ BEGIN
|
|
173
|
+
IF EXISTS (SELECT 1 FROM information_schema.columns
|
|
174
|
+
WHERE table_schema = 'streamby' AND table_name = 'api_tokens' AND column_name = 'access') THEN
|
|
175
|
+
ALTER TABLE streamby.api_tokens ALTER COLUMN access DROP NOT NULL;
|
|
176
|
+
END IF;
|
|
177
|
+
END $$;
|
|
166
178
|
CREATE INDEX IF NOT EXISTS api_tokens_userid_idx ON streamby.api_tokens ("userId");
|
|
179
|
+
CREATE INDEX IF NOT EXISTS api_tokens_kind_idx ON streamby.api_tokens (kind);
|
|
167
180
|
`);
|
|
168
181
|
console.log('✅ "api_tokens" table ensured to exist.');
|
|
169
182
|
// The MCP server's audit log: every tool call (see mcp/audit). Main db only.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { createStreamByRouter } from './middleware/createRouter';
|
|
2
2
|
export { emitToUser } from './services/wsHub';
|
|
3
3
|
export { createNotification } from './services/notification';
|
|
4
|
-
export type { UserPlan, UserSubscription, Auth, ApiTokenScope,
|
|
4
|
+
export type { UserPlan, UserSubscription, Auth, ApiTokenScope, ApiTokenKind, ApiTokenPermission, ProjectPermission, AccountPermission } from './types';
|
|
5
5
|
export { resolveApiToken, API_TOKEN_PREFIX } from './services/apiTokens';
|
|
6
|
+
export { PROJECT_PERMISSIONS, ACCOUNT_PERMISSIONS } from './services/apiTokenPermissions';
|
|
6
7
|
export type { ResolvedApiToken } from './services/apiTokens';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.API_TOKEN_PREFIX = exports.resolveApiToken = exports.createNotification = exports.emitToUser = exports.createStreamByRouter = void 0;
|
|
3
|
+
exports.ACCOUNT_PERMISSIONS = exports.PROJECT_PERMISSIONS = exports.API_TOKEN_PREFIX = exports.resolveApiToken = exports.createNotification = exports.emitToUser = exports.createStreamByRouter = void 0;
|
|
4
4
|
var createRouter_1 = require("./middleware/createRouter");
|
|
5
5
|
Object.defineProperty(exports, "createStreamByRouter", { enumerable: true, get: function () { return createRouter_1.createStreamByRouter; } });
|
|
6
6
|
var wsHub_1 = require("./services/wsHub");
|
|
@@ -10,3 +10,6 @@ Object.defineProperty(exports, "createNotification", { enumerable: true, get: fu
|
|
|
10
10
|
var apiTokens_1 = require("./services/apiTokens");
|
|
11
11
|
Object.defineProperty(exports, "resolveApiToken", { enumerable: true, get: function () { return apiTokens_1.resolveApiToken; } });
|
|
12
12
|
Object.defineProperty(exports, "API_TOKEN_PREFIX", { enumerable: true, get: function () { return apiTokens_1.API_TOKEN_PREFIX; } });
|
|
13
|
+
var apiTokenPermissions_1 = require("./services/apiTokenPermissions");
|
|
14
|
+
Object.defineProperty(exports, "PROJECT_PERMISSIONS", { enumerable: true, get: function () { return apiTokenPermissions_1.PROJECT_PERMISSIONS; } });
|
|
15
|
+
Object.defineProperty(exports, "ACCOUNT_PERMISSIONS", { enumerable: true, get: function () { return apiTokenPermissions_1.ACCOUNT_PERMISSIONS; } });
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -6,9 +6,10 @@ export type ApiCaller = (call: ToolCall) => Promise<{
|
|
|
6
6
|
status: number;
|
|
7
7
|
body: unknown;
|
|
8
8
|
}>;
|
|
9
|
+
/** The tools an API token may use: those whose every permission it has. */
|
|
10
|
+
export declare const toolsFor: (scope: Auth["apiToken"]) => import("./tools").ToolDefinition<any>[];
|
|
9
11
|
/**
|
|
10
|
-
* An MCP server for one request
|
|
11
|
-
*
|
|
12
|
-
* recorded in the audit log.
|
|
12
|
+
* An MCP server for one request made with an API token (`auth`): only the tools its permissions
|
|
13
|
+
* allow. Each tool call goes to the REST API through `api`, and is recorded in the audit log.
|
|
13
14
|
*/
|
|
14
15
|
export declare function createMcpServer(auth: Auth, api: ApiCaller, config: StreamByConfig, version: string): McpServer;
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MCP_SERVER_NAME = void 0;
|
|
3
|
+
exports.toolsFor = exports.MCP_SERVER_NAME = void 0;
|
|
4
4
|
exports.createMcpServer = createMcpServer;
|
|
5
5
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
6
6
|
const tools_1 = require("./tools");
|
|
@@ -10,25 +10,28 @@ const errorText = (body, status) => {
|
|
|
10
10
|
const message = body?.message ?? body?.error;
|
|
11
11
|
return typeof message === 'string' ? message : `StreamBy answered ${status}`;
|
|
12
12
|
};
|
|
13
|
+
/** The tools an API token may use: those whose every permission it has. */
|
|
14
|
+
const toolsFor = (scope) => tools_1.TOOLS.filter(tool => tool.permissions.every(p => !!scope?.permissions.includes(p)));
|
|
15
|
+
exports.toolsFor = toolsFor;
|
|
13
16
|
/**
|
|
14
|
-
* An MCP server for one request
|
|
15
|
-
*
|
|
16
|
-
* recorded in the audit log.
|
|
17
|
+
* An MCP server for one request made with an API token (`auth`): only the tools its permissions
|
|
18
|
+
* allow. Each tool call goes to the REST API through `api`, and is recorded in the audit log.
|
|
17
19
|
*/
|
|
18
20
|
function createMcpServer(auth, api, config, version) {
|
|
19
21
|
const server = new mcp_js_1.McpServer({ name: exports.MCP_SERVER_NAME, version });
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// A project's token acts on its project: what it does there is the project's.
|
|
23
|
+
const tokenProject = auth.apiToken?.kind === 'project' && Array.isArray(auth.apiToken.projectIds)
|
|
24
|
+
? auth.apiToken.projectIds[0] ?? null
|
|
25
|
+
: null;
|
|
26
|
+
for (const tool of (0, exports.toolsFor)(auth.apiToken)) {
|
|
24
27
|
server.registerTool(tool.name, {
|
|
25
28
|
title: tool.title,
|
|
26
29
|
description: tool.description,
|
|
27
30
|
inputSchema: tool.input,
|
|
28
|
-
annotations: { readOnlyHint: !tool.write, destructiveHint:
|
|
31
|
+
annotations: { readOnlyHint: !tool.write, destructiveHint: !!tool.destructive, openWorldHint: false },
|
|
29
32
|
}, async (args) => {
|
|
30
33
|
const started = Date.now();
|
|
31
|
-
const projectId = tool.projectOf?.(args) ??
|
|
34
|
+
const projectId = tool.projectOf?.(args) ?? tokenProject;
|
|
32
35
|
let status = null;
|
|
33
36
|
try {
|
|
34
37
|
const answer = await api(tool.call(args));
|
package/dist/mcp/tools.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { ApiTokenPermission } from '../services/apiTokenPermissions';
|
|
2
3
|
export interface ToolCall {
|
|
3
|
-
method: 'GET' | 'POST' | 'PATCH' | 'PUT';
|
|
4
|
+
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
|
4
5
|
path: string;
|
|
5
6
|
body?: unknown;
|
|
6
7
|
}
|
|
@@ -8,7 +9,9 @@ export interface ToolDefinition<S extends z.ZodRawShape = z.ZodRawShape> {
|
|
|
8
9
|
name: string;
|
|
9
10
|
title: string;
|
|
10
11
|
description: string;
|
|
11
|
-
|
|
12
|
+
permissions: ApiTokenPermission[];
|
|
13
|
+
write?: boolean;
|
|
14
|
+
destructive?: boolean;
|
|
12
15
|
input: S;
|
|
13
16
|
projectOf?: (args: z.infer<z.ZodObject<S>>) => string | undefined;
|
|
14
17
|
call: (args: z.infer<z.ZodObject<S>>) => ToolCall;
|
package/dist/mcp/tools.js
CHANGED
|
@@ -38,21 +38,21 @@ const defined = (fields) => Object.fromEntries(Object.entries(fields).filter(([,
|
|
|
38
38
|
exports.TOOLS = [
|
|
39
39
|
// ── Reading ──
|
|
40
40
|
tool({
|
|
41
|
-
name: 'list_projects', title: 'List projects',
|
|
41
|
+
name: 'list_projects', title: 'List projects', permissions: [],
|
|
42
42
|
description: 'The StreamBy projects this token reaches.',
|
|
43
43
|
input: {},
|
|
44
44
|
call: () => ({ method: 'GET', path: '/projects' }),
|
|
45
45
|
pick: (data) => data.projects,
|
|
46
46
|
}),
|
|
47
47
|
tool({
|
|
48
|
-
name: 'get_project', title: 'Get a project',
|
|
48
|
+
name: 'get_project', title: 'Get a project', permissions: ['project:read'],
|
|
49
49
|
description: "A project's details: its name, description, members, exports and connections.",
|
|
50
50
|
input: { projectId },
|
|
51
51
|
projectOf: byProject,
|
|
52
52
|
call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}` }),
|
|
53
53
|
}),
|
|
54
54
|
tool({
|
|
55
|
-
name: 'list_assets', title: 'List assets',
|
|
55
|
+
name: 'list_assets', title: 'List assets', permissions: ['assets:read'],
|
|
56
56
|
description: "A project's assets of a category (images, audios, videos, 3d-models), optionally in one storage connection, and optionally only those whose name contains `query`.",
|
|
57
57
|
input: {
|
|
58
58
|
projectId,
|
|
@@ -77,42 +77,42 @@ exports.TOOLS = [
|
|
|
77
77
|
},
|
|
78
78
|
}),
|
|
79
79
|
tool({
|
|
80
|
-
name: 'get_asset_metadata', title: "Get an asset's metadata",
|
|
80
|
+
name: 'get_asset_metadata', title: "Get an asset's metadata", permissions: ['assets:read'],
|
|
81
81
|
description: "An asset's metadata (technical data and custom tags).",
|
|
82
82
|
input: { projectId, fileId: id('asset') },
|
|
83
83
|
projectOf: byProject,
|
|
84
84
|
call: ({ projectId, fileId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/assets/${enc(fileId)}/metadata` }),
|
|
85
85
|
}),
|
|
86
86
|
tool({
|
|
87
|
-
name: 'get_asset_rights', title: "Get an asset's rights",
|
|
87
|
+
name: 'get_asset_rights', title: "Get an asset's rights", permissions: ['rights:read'],
|
|
88
88
|
description: "An asset's rights: holder, license, territory, expiry and restrictions.",
|
|
89
89
|
input: { projectId, assetId: id('asset') },
|
|
90
90
|
projectOf: byProject,
|
|
91
91
|
call: ({ projectId, assetId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/assets/${enc(assetId)}/rights` }),
|
|
92
92
|
}),
|
|
93
93
|
tool({
|
|
94
|
-
name: 'get_workflow', title: "Get a project's workflow",
|
|
94
|
+
name: 'get_workflow', title: "Get a project's workflow", permissions: ['workflow:read'],
|
|
95
95
|
description: "A project's workflow (its node graph).",
|
|
96
96
|
input: { projectId },
|
|
97
97
|
projectOf: byProject,
|
|
98
98
|
call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/workflow` }),
|
|
99
99
|
}),
|
|
100
100
|
tool({
|
|
101
|
-
name: 'list_pipelines', title: 'List pipelines',
|
|
101
|
+
name: 'list_pipelines', title: 'List pipelines', permissions: ['workflow:read'],
|
|
102
102
|
description: "A project's pipelines.",
|
|
103
103
|
input: { projectId },
|
|
104
104
|
projectOf: byProject,
|
|
105
105
|
call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/pipelines` }),
|
|
106
106
|
}),
|
|
107
107
|
tool({
|
|
108
|
-
name: 'get_pipeline', title: 'Get a pipeline',
|
|
108
|
+
name: 'get_pipeline', title: 'Get a pipeline', permissions: ['workflow:read'],
|
|
109
109
|
description: 'A pipeline, with its node schema.',
|
|
110
110
|
input: { projectId, pipelineId: id('pipeline') },
|
|
111
111
|
projectOf: byProject,
|
|
112
112
|
call: ({ projectId, pipelineId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/pipelines/${enc(pipelineId)}` }),
|
|
113
113
|
}),
|
|
114
114
|
tool({
|
|
115
|
-
name: 'list_exports', title: 'List exports',
|
|
115
|
+
name: 'list_exports', title: 'List exports', permissions: ['project:read', 'exports:read'],
|
|
116
116
|
description: "A project's exports (its published endpoints).",
|
|
117
117
|
input: { projectId },
|
|
118
118
|
projectOf: byProject,
|
|
@@ -120,34 +120,34 @@ exports.TOOLS = [
|
|
|
120
120
|
pick: (data) => data?.project?.exports ?? data?.exports ?? [],
|
|
121
121
|
}),
|
|
122
122
|
tool({
|
|
123
|
-
name: 'get_export', title: 'Get an export',
|
|
123
|
+
name: 'get_export', title: 'Get an export', permissions: ['exports:read'],
|
|
124
124
|
description: 'An export, with its node schema.',
|
|
125
125
|
input: { projectId, exportId: id('export') },
|
|
126
126
|
projectOf: byProject,
|
|
127
127
|
call: ({ projectId, exportId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/exports/${enc(exportId)}` }),
|
|
128
128
|
}),
|
|
129
129
|
tool({
|
|
130
|
-
name: 'get_job', title: 'Get a job',
|
|
130
|
+
name: 'get_job', title: 'Get a job', permissions: ['jobs:read'],
|
|
131
131
|
description: "A job's status and progress (transcoding, rendering, ingesting...).",
|
|
132
132
|
input: { jobId: id('job') },
|
|
133
133
|
call: ({ jobId }) => ({ method: 'GET', path: `/jobs/${enc(jobId)}` }),
|
|
134
134
|
}),
|
|
135
135
|
tool({
|
|
136
|
-
name: 'list_sequences', title: 'List production sequences',
|
|
136
|
+
name: 'list_sequences', title: 'List production sequences', permissions: ['production:read'],
|
|
137
137
|
description: "A project's production sequences.",
|
|
138
138
|
input: { projectId },
|
|
139
139
|
projectOf: byProject,
|
|
140
140
|
call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/production/sequences` }),
|
|
141
141
|
}),
|
|
142
142
|
tool({
|
|
143
|
-
name: 'list_shots', title: 'List shots',
|
|
143
|
+
name: 'list_shots', title: 'List shots', permissions: ['production:read'],
|
|
144
144
|
description: "A production sequence's shots.",
|
|
145
145
|
input: { projectId, sequenceId: id('sequence') },
|
|
146
146
|
projectOf: byProject,
|
|
147
147
|
call: ({ projectId, sequenceId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/production/sequences/${enc(sequenceId)}/shots` }),
|
|
148
148
|
}),
|
|
149
149
|
tool({
|
|
150
|
-
name: 'list_tasks', title: 'List shot tasks',
|
|
150
|
+
name: 'list_tasks', title: 'List shot tasks', permissions: ['production:read'],
|
|
151
151
|
description: "A shot's tasks.",
|
|
152
152
|
input: { projectId, shotId: id('shot') },
|
|
153
153
|
projectOf: byProject,
|
|
@@ -155,73 +155,132 @@ exports.TOOLS = [
|
|
|
155
155
|
}),
|
|
156
156
|
// ── Writing ──
|
|
157
157
|
tool({
|
|
158
|
-
name: 'update_asset_metadata', title: "Update an asset's custom tags", write: true,
|
|
158
|
+
name: 'update_asset_metadata', title: "Update an asset's custom tags", permissions: ['assets:write'], write: true,
|
|
159
159
|
description: "Sets an asset's custom tags (key/value pairs), replacing the ones it had.",
|
|
160
160
|
input: { projectId, fileId: id('asset'), customTags: zod_1.z.record(zod_1.z.string(), zod_1.z.string()) },
|
|
161
161
|
projectOf: byProject,
|
|
162
162
|
call: ({ projectId, fileId, customTags }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/assets/${enc(fileId)}/metadata`, body: { customTags } }),
|
|
163
163
|
}),
|
|
164
164
|
tool({
|
|
165
|
-
name: 'set_asset_rights', title: "Set an asset's rights", write: true,
|
|
165
|
+
name: 'set_asset_rights', title: "Set an asset's rights", permissions: ['rights:write'], write: true,
|
|
166
166
|
description: "Sets an asset's rights: holder, license, territory, expiry, restrictions and notes.",
|
|
167
167
|
input: { projectId, assetId: id('asset'), ...rightsFields },
|
|
168
168
|
projectOf: byProject,
|
|
169
169
|
call: ({ projectId, assetId, ...rights }) => ({ method: 'PUT', path: `/projects/${enc(projectId)}/assets/${enc(assetId)}/rights`, body: defined(rights) }),
|
|
170
170
|
}),
|
|
171
171
|
tool({
|
|
172
|
-
name: 'create_pipeline', title: 'Create a pipeline', write: true,
|
|
172
|
+
name: 'create_pipeline', title: 'Create a pipeline', permissions: ['workflow:write'], write: true,
|
|
173
173
|
description: 'Creates a pipeline in a project.',
|
|
174
174
|
input: { projectId, name: zod_1.z.string().min(1), description: zod_1.z.string().optional(), order: zod_1.z.number().optional() },
|
|
175
175
|
projectOf: byProject,
|
|
176
176
|
call: ({ projectId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/pipelines`, body: defined(fields) }),
|
|
177
177
|
}),
|
|
178
178
|
tool({
|
|
179
|
-
name: 'update_pipeline', title: 'Update a pipeline', write: true,
|
|
179
|
+
name: 'update_pipeline', title: 'Update a pipeline', permissions: ['workflow:write'], write: true,
|
|
180
180
|
description: "Changes a pipeline's name, description or order.",
|
|
181
181
|
input: { projectId, pipelineId: id('pipeline'), name: zod_1.z.string().min(1).optional(), description: zod_1.z.string().optional(), order: zod_1.z.number().optional() },
|
|
182
182
|
projectOf: byProject,
|
|
183
183
|
call: ({ projectId, pipelineId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/pipelines/${enc(pipelineId)}`, body: defined(fields) }),
|
|
184
184
|
}),
|
|
185
185
|
tool({
|
|
186
|
-
name: 'create_sequence', title: 'Create a production sequence', write: true,
|
|
186
|
+
name: 'create_sequence', title: 'Create a production sequence', permissions: ['production:write'], write: true,
|
|
187
187
|
description: 'Creates a production sequence in a project.',
|
|
188
188
|
input: { projectId, name: zod_1.z.string().min(1), description: zod_1.z.string().optional(), order: zod_1.z.number().optional() },
|
|
189
189
|
projectOf: byProject,
|
|
190
190
|
call: ({ projectId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/production/sequences`, body: defined(fields) }),
|
|
191
191
|
}),
|
|
192
192
|
tool({
|
|
193
|
-
name: 'update_sequence', title: 'Update a production sequence', write: true,
|
|
193
|
+
name: 'update_sequence', title: 'Update a production sequence', permissions: ['production:write'], write: true,
|
|
194
194
|
description: "Changes a sequence's name, description or order.",
|
|
195
195
|
input: { projectId, sequenceId: id('sequence'), name: zod_1.z.string().min(1).optional(), description: zod_1.z.string().optional(), order: zod_1.z.number().optional() },
|
|
196
196
|
projectOf: byProject,
|
|
197
197
|
call: ({ projectId, sequenceId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/production/sequences/${enc(sequenceId)}`, body: defined(fields) }),
|
|
198
198
|
}),
|
|
199
199
|
tool({
|
|
200
|
-
name: 'create_shot', title: 'Create a shot', write: true,
|
|
200
|
+
name: 'create_shot', title: 'Create a shot', permissions: ['production:write'], write: true,
|
|
201
201
|
description: 'Creates a shot in a production sequence.',
|
|
202
202
|
input: { projectId, sequenceId: id('sequence'), name: zod_1.z.string().min(1), ...shotFields },
|
|
203
203
|
projectOf: byProject,
|
|
204
204
|
call: ({ projectId, sequenceId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/production/sequences/${enc(sequenceId)}/shots`, body: defined(fields) }),
|
|
205
205
|
}),
|
|
206
206
|
tool({
|
|
207
|
-
name: 'update_shot', title: 'Update a shot', write: true,
|
|
207
|
+
name: 'update_shot', title: 'Update a shot', permissions: ['production:write'], write: true,
|
|
208
208
|
description: 'Changes a shot (its name, status, assignee, due date...).',
|
|
209
209
|
input: { projectId, shotId: id('shot'), name: zod_1.z.string().min(1).optional(), ...shotFields },
|
|
210
210
|
projectOf: byProject,
|
|
211
211
|
call: ({ projectId, shotId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}`, body: defined(fields) }),
|
|
212
212
|
}),
|
|
213
213
|
tool({
|
|
214
|
-
name: 'create_task', title: 'Create a shot task', write: true,
|
|
214
|
+
name: 'create_task', title: 'Create a shot task', permissions: ['production:write'], write: true,
|
|
215
215
|
description: 'Creates a task in a shot.',
|
|
216
216
|
input: { projectId, shotId: id('shot'), name: zod_1.z.string().min(1), ...taskFields },
|
|
217
217
|
projectOf: byProject,
|
|
218
218
|
call: ({ projectId, shotId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}/tasks`, body: defined(fields) }),
|
|
219
219
|
}),
|
|
220
220
|
tool({
|
|
221
|
-
name: 'update_task', title: 'Update a shot task', write: true,
|
|
221
|
+
name: 'update_task', title: 'Update a shot task', permissions: ['production:write'], write: true,
|
|
222
222
|
description: 'Changes a task (its name, status, priority, assignee, due date, notes).',
|
|
223
223
|
input: { projectId, shotId: id('shot'), taskId: id('task'), name: zod_1.z.string().min(1).optional(), ...taskFields },
|
|
224
224
|
projectOf: byProject,
|
|
225
225
|
call: ({ projectId, shotId, taskId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}/tasks/${enc(taskId)}`, body: defined(fields) }),
|
|
226
226
|
}),
|
|
227
|
+
// ── Projects ──
|
|
228
|
+
tool({
|
|
229
|
+
name: 'create_project', title: 'Create a project', permissions: ['projects:create'], write: true,
|
|
230
|
+
description: 'Creates a StreamBy project, its maker being its admin, as from "New project" in the UI. The token reaches it from then on.',
|
|
231
|
+
input: {
|
|
232
|
+
name: zod_1.z.string().min(1),
|
|
233
|
+
description: zod_1.z.string().optional(),
|
|
234
|
+
public: zod_1.z.boolean().optional().describe('Whether it shows in Explore (true if not given; a private one needs a subscriber plan)'),
|
|
235
|
+
category: zod_1.z.string().optional(),
|
|
236
|
+
},
|
|
237
|
+
call: (fields) => ({ method: 'POST', path: '/projects/create', body: defined(fields) }),
|
|
238
|
+
pick: (data) => ({ projectId: data?.projectId, message: data?.message }),
|
|
239
|
+
}),
|
|
240
|
+
tool({
|
|
241
|
+
name: 'delete_project', title: 'Delete a project', permissions: ['project:delete'], write: true, destructive: true,
|
|
242
|
+
description: 'Deletes a project, with everything in it, for every member. It cannot be undone.',
|
|
243
|
+
input: { projectId },
|
|
244
|
+
projectOf: byProject,
|
|
245
|
+
call: ({ projectId }) => ({ method: 'DELETE', path: `/projects/${enc(projectId)}` }),
|
|
246
|
+
pick: (data) => ({ message: data?.message }),
|
|
247
|
+
}),
|
|
248
|
+
// ── Deleting ──
|
|
249
|
+
tool({
|
|
250
|
+
name: 'delete_asset', title: 'Delete an asset', permissions: ['assets:delete'], write: true, destructive: true,
|
|
251
|
+
description: "Deletes an asset's file, from the project's built-in storage or from one of its storage connections. It cannot be undone.",
|
|
252
|
+
input: {
|
|
253
|
+
projectId,
|
|
254
|
+
fileId: id('asset'),
|
|
255
|
+
connectionId: zod_1.z.string().optional().describe("The storage connection it's in; the project's built-in storage if not given"),
|
|
256
|
+
},
|
|
257
|
+
projectOf: byProject,
|
|
258
|
+
call: ({ projectId, fileId, connectionId }) => ({
|
|
259
|
+
method: 'DELETE',
|
|
260
|
+
path: connectionId
|
|
261
|
+
? `/projects/${enc(projectId)}/connections/storage/${enc(connectionId)}/files/${enc(fileId)}`
|
|
262
|
+
: `/projects/${enc(projectId)}/storage/files/${enc(fileId)}`,
|
|
263
|
+
}),
|
|
264
|
+
}),
|
|
265
|
+
tool({
|
|
266
|
+
name: 'delete_sequence', title: 'Delete a production sequence', permissions: ['production:delete'], write: true, destructive: true,
|
|
267
|
+
description: 'Deletes a production sequence, with its shots. It cannot be undone.',
|
|
268
|
+
input: { projectId, sequenceId: id('sequence') },
|
|
269
|
+
projectOf: byProject,
|
|
270
|
+
call: ({ projectId, sequenceId }) => ({ method: 'DELETE', path: `/projects/${enc(projectId)}/production/sequences/${enc(sequenceId)}` }),
|
|
271
|
+
}),
|
|
272
|
+
tool({
|
|
273
|
+
name: 'delete_shot', title: 'Delete a shot', permissions: ['production:delete'], write: true, destructive: true,
|
|
274
|
+
description: 'Deletes a shot, with its tasks. It cannot be undone.',
|
|
275
|
+
input: { projectId, shotId: id('shot') },
|
|
276
|
+
projectOf: byProject,
|
|
277
|
+
call: ({ projectId, shotId }) => ({ method: 'DELETE', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}` }),
|
|
278
|
+
}),
|
|
279
|
+
tool({
|
|
280
|
+
name: 'delete_task', title: 'Delete a shot task', permissions: ['production:delete'], write: true, destructive: true,
|
|
281
|
+
description: "Deletes a shot's task. It cannot be undone.",
|
|
282
|
+
input: { projectId, shotId: id('shot'), taskId: id('task') },
|
|
283
|
+
projectOf: byProject,
|
|
284
|
+
call: ({ projectId, shotId, taskId }) => ({ method: 'DELETE', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}/tasks/${enc(taskId)}` }),
|
|
285
|
+
}),
|
|
227
286
|
];
|