streamby-core 0.35.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 CHANGED
@@ -1,5 +1,69 @@
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
+
37
+ ## 0.36.0
38
+
39
+ ### Added
40
+
41
+ - **MCP server** at `POST /mcp`, for agents such as Claude Code to work with StreamBy projects
42
+ with an API token (see the README).
43
+ - It takes only an API token.
44
+ - Its tools call the REST API with that token, so they reach exactly what the token and its
45
+ owner can, and writes are saved as the UI's are.
46
+ - **Reading tools:** projects, assets with their metadata and rights, workflow, pipelines,
47
+ exports, jobs, and production sequences, shots and tasks.
48
+ - **Writing tools**, listed only for `write` tokens: asset metadata and rights, pipelines,
49
+ sequences, shots and tasks.
50
+ - **`mcp.rateLimit`** (default 120 requests a minute per token; past it, 429 with
51
+ `Retry-After`) and **`mcp.apiBaseUrl`** in the router config.
52
+ - **New table: `mcp_audit`.** It's created at startup in the main database. Every tool call is
53
+ recorded there, with the user, token, project, tool and result, but never the arguments nor
54
+ the token.
55
+ - New dependencies: `@modelcontextprotocol/sdk` and `zod`.
56
+
57
+ ### Fixed
58
+
59
+ - **Projects can be made with PostgreSQL as the main database.** The `projects` table that
60
+ StreamBy creates at startup was missing columns the routes write: `public`, `category`,
61
+ `integrationConnections`, `distributionConnections` and `renderFarmConnections`.
62
+ - Making a project failed with `column "public" does not exist`.
63
+ - Adding service, distribution or render farm connections failed too.
64
+ - They're added at startup now, only where they're missing; databases that already have
65
+ them aren't changed.
66
+
3
67
  ## 0.35.0
4
68
 
5
69
  ### 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 as a user through API tokens, without the user's
106
- browser session. A user manages their own tokens (`GET`, `POST` and
107
- `DELETE /streamby/user/api-tokens`, only with a session). Each token has:
108
-
109
- - **an access level**: `read` (only reads) or `write` (reads and writes);
110
- - **the projects it reaches**: a list of project ids, or `'*'` for every project the user is a
111
- member of, including ones they join later;
112
- - **an expiry**: 7, 30, 90 or 365 days, or none.
113
-
114
- A token is `sbt_` followed by 32 random bytes. It's shown once, when it's made. Only its
115
- SHA-256 hash is stored, with a short prefix to tell it apart.
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` and returns its owner, with the token's scope in `apiToken`:
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,114 @@ createStreamByRouter({
135
167
  });
136
168
  ```
137
169
 
138
- A request made with a token reaches only what the token's scope allows:
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.
173
+
174
+ ---
175
+
176
+ ## 🤖 MCP server
177
+
178
+ StreamBy serves an [MCP](https://modelcontextprotocol.io) server at `POST /streamby/mcp`
179
+ (Streamable HTTP, stateless). Agents such as Claude Code use it to read and work on StreamBy
180
+ projects with an API token (see above). It takes only an API token; a browser session gets 401.
181
+
182
+ Each tool calls StreamBy's own REST API with the caller's token. So it reaches exactly what the
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.
185
+
186
+ **Tools**
187
+
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.
211
+
212
+ **Limits and audit**
213
+
214
+ - Each token can make at most 120 requests a minute (a sliding window). Past that, StreamBy
215
+ answers 429 with `Retry-After`. Each instance counts its own requests. Change the limit with
216
+ `mcp.rateLimit`.
217
+ - Every tool call is recorded in the `mcp_audit` table, created at startup in the main database.
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.
221
+
222
+ ```ts
223
+ createStreamByRouter({
224
+ // ...
225
+ mcp: {
226
+ rateLimit: { requests: 120, windowMs: 60_000 }, // the default
227
+ // apiBaseUrl: 'http://127.0.0.1:3000/streamby', // where tools reach the REST API;
228
+ // by default this same process, at the request's local port and mount path
229
+ },
230
+ });
231
+ ```
232
+
233
+ ### Connecting from Claude Code
234
+
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:
139
238
 
140
- - a `read` token can't write;
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`).
239
+ ```bash
240
+ export STREAMBY_TOKEN=sbt_...
241
+ ```
242
+
243
+ Add the server to your project's `.mcp.json`. Claude Code expands `${STREAMBY_TOKEN}` from the
244
+ environment:
245
+
246
+ ```json
247
+ {
248
+ "mcpServers": {
249
+ "streamby": {
250
+ "type": "http",
251
+ "url": "https://your-host/streamby/mcp",
252
+ "headers": { "Authorization": "Bearer ${STREAMBY_TOKEN}" }
253
+ }
254
+ }
255
+ }
256
+ ```
257
+
258
+ ### Connecting from Claude Desktop
259
+
260
+ Claude Desktop runs local (stdio) servers from `claude_desktop_config.json`. Reach StreamBy
261
+ through [`mcp-remote`](https://www.npmjs.com/package/mcp-remote), passing the token in its
262
+ `env`. This file holds it in plain text, so use a token with only the permissions you need,
263
+ and an expiry:
264
+
265
+ ```json
266
+ {
267
+ "mcpServers": {
268
+ "streamby": {
269
+ "command": "npx",
270
+ "args": ["mcp-remote", "https://your-host/streamby/mcp", "--header", "Authorization:${STREAMBY_AUTH}"],
271
+ "env": { "STREAMBY_AUTH": "Bearer sbt_..." }
272
+ }
273
+ }
274
+ }
275
+ ```
146
276
 
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.
277
+ The claude.ai connector needs OAuth, which isn't available yet: it's for API tokens only for now.
149
278
 
150
279
  ---
151
280
 
@@ -80,11 +80,12 @@ const ensureTablesExist = async (pool) => {
80
80
  ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS workflow JSONB;
81
81
  ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS pipelines JSONB DEFAULT '[]';
82
82
  ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "builtinBackfilledAt" TIMESTAMP WITH TIME ZONE;
83
- -- Reverted: a project has no notion of a "default" database/storage connection —
84
- -- each dataSourceNode/ingestNode always declares its own connection explicitly.
85
- ALTER TABLE streamby.projects DROP COLUMN IF EXISTS "defaultDatabaseId";
86
- ALTER TABLE streamby.projects DROP COLUMN IF EXISTS "defaultStorageId";
87
- ALTER TABLE streamby.projects DROP COLUMN IF EXISTS "defaultConnectionsBackfilledAt";
83
+ -- Written when a project is made (routes/project) and by its connection routes.
84
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS public BOOLEAN DEFAULT TRUE;
85
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS category TEXT;
86
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "integrationConnections" JSONB DEFAULT '[]';
87
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "distributionConnections" JSONB DEFAULT '[]';
88
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "renderFarmConnections" JSONB DEFAULT '[]';
88
89
  `);
89
90
  // Ensure dbType exists with a non-null default
90
91
  await pool.query(`
@@ -148,23 +149,53 @@ const ensureTablesExist = async (pool) => {
148
149
  `);
149
150
  console.log('✅ "user_integrations" table ensured to exist.');
150
151
  // API tokens: only their hash is kept (see services/apiTokens). Main db only, like
151
- // 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.
152
154
  await pool.query(`
153
155
  CREATE TABLE IF NOT EXISTS streamby.api_tokens (
154
- id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
155
- "userId" VARCHAR(255) NOT NULL,
156
- name VARCHAR(255) NOT NULL,
157
- prefix VARCHAR(32) NOT NULL,
158
- hash CHAR(64) NOT NULL UNIQUE,
159
- access VARCHAR(10) NOT NULL,
160
- "projectIds" JSONB NOT NULL,
161
- "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
162
- "expiresAt" TIMESTAMP WITH TIME ZONE,
163
- "lastUsedAt" TIMESTAMP WITH TIME ZONE
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
164
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 $$;
165
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);
166
180
  `);
167
181
  console.log('✅ "api_tokens" table ensured to exist.');
182
+ // The MCP server's audit log: every tool call (see mcp/audit). Main db only.
183
+ await pool.query(`
184
+ CREATE TABLE IF NOT EXISTS streamby.mcp_audit (
185
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
186
+ "userId" VARCHAR(255) NOT NULL,
187
+ "tokenId" VARCHAR(255) NOT NULL,
188
+ "projectId" VARCHAR(255),
189
+ tool VARCHAR(100) NOT NULL,
190
+ ok BOOLEAN NOT NULL,
191
+ status INTEGER,
192
+ error TEXT,
193
+ "durationMs" INTEGER NOT NULL,
194
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
195
+ );
196
+ CREATE INDEX IF NOT EXISTS mcp_audit_token_idx ON streamby.mcp_audit ("tokenId", "createdAt");
197
+ `);
198
+ console.log('✅ "mcp_audit" table ensured to exist.');
168
199
  }
169
200
  catch (error) {
170
201
  console.error('❌ Error ensuring tables exist:', error);
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, ApiTokenAccess } from './types';
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; } });
@@ -0,0 +1,12 @@
1
+ import { StreamByConfig } from '../types';
2
+ export interface McpAuditEntry {
3
+ userId: string;
4
+ tokenId: string;
5
+ projectId: string | null;
6
+ tool: string;
7
+ ok: boolean;
8
+ status: number | null;
9
+ error: string | null;
10
+ durationMs: number;
11
+ }
12
+ export declare function recordMcpCall(entry: McpAuditEntry, config: StreamByConfig): Promise<void>;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.recordMcpCall = recordMcpCall;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const manager_1 = require("../models/manager");
9
+ async function recordMcpCall(entry, config) {
10
+ try {
11
+ const dbType = config.databases?.find(db => db.main)?.type ?? 'nosql';
12
+ await (0, manager_1.getModel)('mcp_audit').create({ id: crypto_1.default.randomUUID(), ...entry, createdAt: new Date(), dbType });
13
+ }
14
+ catch (err) {
15
+ console.error('❌ StreamBy MCP: failed to record a tool call:', err instanceof Error ? err.message : err);
16
+ }
17
+ }
@@ -0,0 +1,14 @@
1
+ export interface RateLimit {
2
+ requests: number;
3
+ windowMs: number;
4
+ }
5
+ export declare const DEFAULT_MCP_RATE_LIMIT: RateLimit;
6
+ export declare function createRateLimiter({ requests, windowMs }: RateLimit): {
7
+ /** Counts a request by `key`: whether it's allowed, and if not, how long until it would be. */
8
+ take(key: string, now?: number): {
9
+ allowed: true;
10
+ } | {
11
+ allowed: false;
12
+ retryAfterMs: number;
13
+ };
14
+ };
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_MCP_RATE_LIMIT = void 0;
4
+ exports.createRateLimiter = createRateLimiter;
5
+ exports.DEFAULT_MCP_RATE_LIMIT = { requests: 120, windowMs: 60000 };
6
+ function createRateLimiter({ requests, windowMs }) {
7
+ const seen = new Map();
8
+ return {
9
+ /** Counts a request by `key`: whether it's allowed, and if not, how long until it would be. */
10
+ take(key, now = Date.now()) {
11
+ const recent = (seen.get(key) ?? []).filter(at => at > now - windowMs);
12
+ if (recent.length >= requests) {
13
+ seen.set(key, recent);
14
+ return { allowed: false, retryAfterMs: recent[0] + windowMs - now };
15
+ }
16
+ recent.push(now);
17
+ seen.set(key, recent);
18
+ return { allowed: true };
19
+ },
20
+ };
21
+ }
@@ -0,0 +1,15 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { Auth, StreamByConfig } from '../types';
3
+ import { ToolCall } from './tools';
4
+ export declare const MCP_SERVER_NAME = "streamby";
5
+ export type ApiCaller = (call: ToolCall) => Promise<{
6
+ status: number;
7
+ body: unknown;
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>[];
11
+ /**
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.
14
+ */
15
+ export declare function createMcpServer(auth: Auth, api: ApiCaller, config: StreamByConfig, version: string): McpServer;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toolsFor = exports.MCP_SERVER_NAME = void 0;
4
+ exports.createMcpServer = createMcpServer;
5
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
6
+ const tools_1 = require("./tools");
7
+ const audit_1 = require("./audit");
8
+ exports.MCP_SERVER_NAME = 'streamby';
9
+ const errorText = (body, status) => {
10
+ const message = body?.message ?? body?.error;
11
+ return typeof message === 'string' ? message : `StreamBy answered ${status}`;
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;
16
+ /**
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.
19
+ */
20
+ function createMcpServer(auth, api, config, version) {
21
+ const server = new mcp_js_1.McpServer({ name: exports.MCP_SERVER_NAME, version });
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)) {
27
+ server.registerTool(tool.name, {
28
+ title: tool.title,
29
+ description: tool.description,
30
+ inputSchema: tool.input,
31
+ annotations: { readOnlyHint: !tool.write, destructiveHint: !!tool.destructive, openWorldHint: false },
32
+ }, async (args) => {
33
+ const started = Date.now();
34
+ const projectId = tool.projectOf?.(args) ?? tokenProject;
35
+ let status = null;
36
+ try {
37
+ const answer = await api(tool.call(args));
38
+ status = answer.status;
39
+ const ok = status >= 200 && status < 300;
40
+ await (0, audit_1.recordMcpCall)({
41
+ userId: auth.userId, tokenId: auth.apiToken.tokenId, projectId, tool: tool.name,
42
+ ok, status, error: ok ? null : errorText(answer.body, status), durationMs: Date.now() - started,
43
+ }, config);
44
+ if (!ok)
45
+ return { isError: true, content: [{ type: 'text', text: errorText(answer.body, status) }] };
46
+ const result = tool.pick ? tool.pick(answer.body, args) : answer.body;
47
+ return { content: [{ type: 'text', text: JSON.stringify(result ?? null, null, 2) }] };
48
+ }
49
+ catch (err) {
50
+ const message = err instanceof Error ? err.message : String(err);
51
+ await (0, audit_1.recordMcpCall)({
52
+ userId: auth.userId, tokenId: auth.apiToken.tokenId, projectId, tool: tool.name,
53
+ ok: false, status, error: message, durationMs: Date.now() - started,
54
+ }, config);
55
+ return { isError: true, content: [{ type: 'text', text: `The call failed: ${message}` }] };
56
+ }
57
+ });
58
+ }
59
+ return server;
60
+ }
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ import { ApiTokenPermission } from '../services/apiTokenPermissions';
3
+ export interface ToolCall {
4
+ method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
5
+ path: string;
6
+ body?: unknown;
7
+ }
8
+ export interface ToolDefinition<S extends z.ZodRawShape = z.ZodRawShape> {
9
+ name: string;
10
+ title: string;
11
+ description: string;
12
+ permissions: ApiTokenPermission[];
13
+ write?: boolean;
14
+ destructive?: boolean;
15
+ input: S;
16
+ projectOf?: (args: z.infer<z.ZodObject<S>>) => string | undefined;
17
+ call: (args: z.infer<z.ZodObject<S>>) => ToolCall;
18
+ pick?: (data: any, args: z.infer<z.ZodObject<S>>) => unknown;
19
+ }
20
+ export declare const TOOLS: ToolDefinition<any>[];