nubase_cli 0.1.0 → 0.1.2

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.
@@ -7,6 +7,45 @@ export class NubaseClient {
7
7
  capabilities() {
8
8
  return this.request('/agent/v1/capabilities');
9
9
  }
10
+ // --- One-shot backend snapshot -----------------------------------------
11
+ // Aggregates the read-only state an agent needs to start work into a single
12
+ // round-trip: capabilities, schema, buckets, auth users, and gateway keys.
13
+ // Each section degrades to { error } so an unauthorized or unsupported area
14
+ // never blocks the rest of the snapshot.
15
+ async overview(args = {}) {
16
+ const schema = typeof args.schema === 'string' && args.schema ? args.schema : 'public';
17
+ const [capabilities, database, storage, auth, aiGateway] = await Promise.all([
18
+ safeSection(() => this.capabilities()),
19
+ safeSection(() => this.dbExportSchema({ schema })),
20
+ safeSection(() => this.storageListBuckets({ limit: 100 })),
21
+ safeSection(() => this.authListUsers({ perPage: 1 })),
22
+ safeSection(() => this.gatewayListKeys()),
23
+ ]);
24
+ return {
25
+ nubaseUrl: this.config.nubaseUrl,
26
+ project: {
27
+ keyConfigured: Boolean(this.config.projectKey),
28
+ userScoped: Boolean(this.config.userJwt),
29
+ agentId: this.config.agentId,
30
+ },
31
+ permissions: {
32
+ sqlExecute: this.config.allowSqlExecute,
33
+ dangerousSql: this.config.allowDangerousSql,
34
+ adminWrite: this.config.allowAdminWrite,
35
+ },
36
+ capabilities,
37
+ database: { schema, ...database },
38
+ storage,
39
+ auth,
40
+ aiGateway,
41
+ nextSteps: [
42
+ 'Call memory_context({ task }) before planning to recall prior decisions.',
43
+ 'Inspect database above (or db_export_schema) before any schema change; sql_dry_run before sql_execute.',
44
+ 'Use /rest/v1, /auth/v1, /storage/v1 in generated app code; service-role keys stay server-side.',
45
+ 'Write durable decisions with memory_write when the task is done.',
46
+ ],
47
+ };
48
+ }
10
49
  instructions() {
11
50
  return this.request('/agent/v1/instructions');
12
51
  }
@@ -42,6 +81,93 @@ export class NubaseClient {
42
81
  const query = typeof args.query === 'string' && args.query ? args.query : 'select=*';
43
82
  return this.request(`/rest/v1/${encodeURIComponent(table)}?${query}`);
44
83
  }
84
+ // --- Storage (Supabase-style /storage/v1) -------------------------------
85
+ storageListBuckets(args) {
86
+ const query = buildQuery({ search: args.search, limit: args.limit, offset: args.offset });
87
+ return this.request(`/storage/v1/bucket${query}`);
88
+ }
89
+ storageCreateBucket(args) {
90
+ const name = requiredString(args.name, 'name');
91
+ return this.guardedWrite('create bucket', () => this.request('/storage/v1/bucket', {
92
+ method: 'POST',
93
+ body: {
94
+ name,
95
+ public: args.public === true,
96
+ file_size_limit: typeof args.fileSizeLimit === 'number' ? args.fileSizeLimit : undefined,
97
+ },
98
+ }));
99
+ }
100
+ storageDeleteBucket(args) {
101
+ const bucketId = requiredString(args.bucketId, 'bucketId');
102
+ return this.guardedWrite('delete bucket', () => this.request(`/storage/v1/bucket/${encodeURIComponent(bucketId)}`, { method: 'DELETE' }));
103
+ }
104
+ // --- Auth admin (Supabase-style /auth/v1/admin) -------------------------
105
+ authListUsers(args) {
106
+ const query = buildQuery({ page: args.page, per_page: args.perPage, keyword: args.keyword });
107
+ return this.request(`/auth/v1/admin/users${query}`);
108
+ }
109
+ authCreateUser(args) {
110
+ const email = requiredString(args.email, 'email');
111
+ return this.guardedWrite('create user', () => this.request('/auth/v1/admin/users', {
112
+ method: 'POST',
113
+ body: {
114
+ email,
115
+ password: typeof args.password === 'string' ? args.password : undefined,
116
+ phone: typeof args.phone === 'string' ? args.phone : undefined,
117
+ role: typeof args.role === 'string' ? args.role : undefined,
118
+ },
119
+ }));
120
+ }
121
+ authDeleteUser(args) {
122
+ const userId = requiredString(args.userId, 'userId');
123
+ const query = buildQuery({ should_soft_delete: args.softDelete === true ? 'true' : undefined });
124
+ return this.guardedWrite('delete user', () => this.request(`/auth/v1/admin/users/${encodeURIComponent(userId)}${query}`, { method: 'DELETE' }));
125
+ }
126
+ // --- Database schema introspection --------------------------------------
127
+ dbExportSchema(args) {
128
+ return this.request('/auth/v1/admin/schema/export-ddl', {
129
+ method: 'POST',
130
+ body: {
131
+ schemaName: typeof args.schema === 'string' && args.schema ? args.schema : 'public',
132
+ tableNames: typeof args.tables === 'string' ? args.tables : undefined,
133
+ includeDropStatements: args.includeDrop === true,
134
+ },
135
+ });
136
+ }
137
+ // --- AI Gateway control plane (/ai-gateway/admin/v1) --------------------
138
+ gatewayListKeys() {
139
+ return this.request('/ai-gateway/admin/v1/keys');
140
+ }
141
+ gatewayIssueKey(args) {
142
+ return this.guardedWrite('issue gateway key', () => this.request('/ai-gateway/admin/v1/keys', {
143
+ method: 'POST',
144
+ body: {
145
+ name: typeof args.name === 'string' && args.name ? args.name : 'Untitled key',
146
+ description: typeof args.description === 'string' ? args.description : undefined,
147
+ expiresAt: typeof args.expiresAt === 'string' ? args.expiresAt : undefined,
148
+ },
149
+ }));
150
+ }
151
+ gatewayRevokeKey(args) {
152
+ const id = requiredString(args.id, 'id');
153
+ return this.guardedWrite('revoke gateway key', () => this.request(`/ai-gateway/admin/v1/keys/${encodeURIComponent(id)}`, { method: 'DELETE' }));
154
+ }
155
+ gatewayUsage(args) {
156
+ const query = buildQuery({ start_date: args.startDate, end_date: args.endDate });
157
+ return this.request(`/ai-gateway/admin/v1/usage/overview${query}`);
158
+ }
159
+ async guardedWrite(action, run) {
160
+ if (!this.config.allowAdminWrite) {
161
+ return {
162
+ success: false,
163
+ code: 'PERMISSION_GATE_OFF',
164
+ error: `Cannot ${action}: this is supported but admin writes are gated off by default. This is a permission switch, not a missing feature — it will work once enabled.`,
165
+ remedy: 'Set NUBASE_ALLOW_ADMIN_WRITE=true in the MCP bridge env, then retry.',
166
+ userAction: `Ask the user to enable admin writes (NUBASE_ALLOW_ADMIN_WRITE=true) so you can ${action}.`,
167
+ };
168
+ }
169
+ return run();
170
+ }
45
171
  sqlDryRun(args) {
46
172
  const sql = requiredString(args.sql, 'sql');
47
173
  const risk = classifySql(sql);
@@ -58,14 +184,20 @@ export class NubaseClient {
58
184
  if (!this.config.allowSqlExecute) {
59
185
  return {
60
186
  success: false,
61
- error: 'SQL execution is disabled. Set NUBASE_ALLOW_SQL_EXECUTE=true to enable it.',
187
+ code: 'PERMISSION_GATE_OFF',
188
+ error: 'SQL execution is supported but gated off by default. This is a permission switch, not a missing feature.',
189
+ remedy: 'Set NUBASE_ALLOW_SQL_EXECUTE=true in the MCP bridge env, then retry.',
190
+ userAction: 'Ask the user to enable SQL execution (NUBASE_ALLOW_SQL_EXECUTE=true) before retrying.',
62
191
  dryRun,
63
192
  };
64
193
  }
65
194
  if (dryRun.risk === 'DANGEROUS' && !this.config.allowDangerousSql) {
66
195
  return {
67
196
  success: false,
68
- error: 'Dangerous SQL is blocked. Set NUBASE_ALLOW_DANGEROUS_SQL=true to allow it.',
197
+ code: 'DANGEROUS_SQL_BLOCKED',
198
+ error: 'This statement is classified DANGEROUS (e.g. drop/truncate/bulk delete) and is blocked by default.',
199
+ remedy: 'Set NUBASE_ALLOW_DANGEROUS_SQL=true to allow it, and confirm the operation with the user first.',
200
+ userAction: 'Confirm the destructive intent with the user, then ask them to set NUBASE_ALLOW_DANGEROUS_SQL=true.',
69
201
  dryRun,
70
202
  };
71
203
  }
@@ -73,7 +205,60 @@ export class NubaseClient {
73
205
  method: 'POST',
74
206
  body: { query: sql },
75
207
  });
76
- return { risk: dryRun.risk, ...result };
208
+ const out = { risk: dryRun.risk, ...result };
209
+ // Audit trail: record schema-changing executions so they can be reviewed
210
+ // and replayed later. Best-effort — a failure here never fails the execute.
211
+ const isSchemaChange = dryRun.risk === 'SCHEMA_WRITE' || dryRun.risk === 'DANGEROUS';
212
+ const succeeded = !result || result.success !== false;
213
+ if (this.config.recordMigrations !== false && isSchemaChange && succeeded) {
214
+ try {
215
+ await this.recordMigration({ sql, risk: dryRun.risk, statementCount: dryRun.statementCount });
216
+ out.migrationRecorded = true;
217
+ }
218
+ catch (err) {
219
+ out.migrationRecorded = false;
220
+ out.migrationError = err instanceof Error ? err.message : String(err);
221
+ }
222
+ }
223
+ return out;
224
+ }
225
+ // Append-only audit table in a dedicated `nubase` schema (kept out of public
226
+ // so it does not clutter the user's app schema). Ensured idempotently.
227
+ async recordMigration(entry) {
228
+ const query = `create schema if not exists nubase;
229
+ create table if not exists nubase.migrations (
230
+ id bigint generated always as identity primary key,
231
+ applied_at timestamptz not null default now(),
232
+ risk text not null,
233
+ statement_count integer not null default 1,
234
+ sql text not null,
235
+ agent_id text,
236
+ run_id text,
237
+ user_id text
238
+ );
239
+ insert into nubase.migrations (risk, statement_count, sql, agent_id, run_id, user_id)
240
+ values (${sqlLiteral(entry.risk)}, ${entry.statementCount}, ${sqlLiteral(entry.sql)}, ${sqlLiteralOrNull(this.config.agentId)}, ${sqlLiteralOrNull(this.config.runId)}, ${sqlLiteralOrNull(this.config.userId)});`;
241
+ return this.request('/auth/v1/admin/sql/execute', { method: 'POST', body: { query } });
242
+ }
243
+ // Read the audit trail. Hardcoded SELECT against our own table, so it does not
244
+ // require NUBASE_ALLOW_SQL_EXECUTE (reading the log is always safe).
245
+ async listMigrations(args = {}) {
246
+ const limit = typeof args.limit === 'number' && args.limit > 0 ? Math.min(Math.floor(args.limit), 200) : 50;
247
+ try {
248
+ return await this.request('/auth/v1/admin/sql/execute', {
249
+ method: 'POST',
250
+ body: {
251
+ query: `select id, applied_at, risk, statement_count, sql, agent_id, run_id, user_id from nubase.migrations order by applied_at desc limit ${limit};`,
252
+ },
253
+ });
254
+ }
255
+ catch (err) {
256
+ const message = err instanceof Error ? err.message : String(err);
257
+ if (/does not exist|nubase\.migrations/i.test(message)) {
258
+ return { migrations: [], note: 'No migrations recorded yet (nubase.migrations not present).' };
259
+ }
260
+ throw err;
261
+ }
77
262
  }
78
263
  async request(path, options = {}) {
79
264
  if (!this.config.projectKey) {
@@ -99,6 +284,14 @@ export class NubaseClient {
99
284
  return data;
100
285
  }
101
286
  }
287
+ async function safeSection(run) {
288
+ try {
289
+ return await run();
290
+ }
291
+ catch (err) {
292
+ return { error: err instanceof Error ? err.message : String(err) };
293
+ }
294
+ }
102
295
  function parseResponse(text) {
103
296
  if (!text)
104
297
  return null;
@@ -109,9 +302,26 @@ function parseResponse(text) {
109
302
  return text;
110
303
  }
111
304
  }
305
+ function buildQuery(params) {
306
+ const search = new URLSearchParams();
307
+ for (const [key, value] of Object.entries(params)) {
308
+ if (value === undefined || value === null || value === '')
309
+ continue;
310
+ search.set(key, String(value));
311
+ }
312
+ const query = search.toString();
313
+ return query ? `?${query}` : '';
314
+ }
112
315
  function requiredString(value, name) {
113
316
  if (typeof value !== 'string' || !value.trim()) {
114
317
  throw new Error(`${name} is required`);
115
318
  }
116
319
  return value;
117
320
  }
321
+ // Postgres string literal with single quotes doubled — safe for arbitrary text.
322
+ function sqlLiteral(value) {
323
+ return `'${value.replace(/'/g, "''")}'`;
324
+ }
325
+ function sqlLiteralOrNull(value) {
326
+ return value === undefined || value === null ? 'NULL' : sqlLiteral(String(value));
327
+ }
package/dist/src/tools.js CHANGED
@@ -3,7 +3,7 @@ import { fetchDocs } from './docs.js';
3
3
  export const TOOLS = [
4
4
  {
5
5
  name: 'fetch_docs',
6
- description: 'Fetch bundled Nubase agent docs. Topics: overview, setup, memory, database, auth, storage, ai_gateway, supabase_compatibility, security, or all.',
6
+ description: 'Fetch bundled Nubase agent docs. Topics: overview, quickstart, setup, memory, database, auth, storage, ai_gateway, security, or all.',
7
7
  inputSchema: objectSchema({
8
8
  topic: { type: 'string' },
9
9
  }),
@@ -18,6 +18,13 @@ export const TOOLS = [
18
18
  description: 'Return agent instructions for using Nubase safely.',
19
19
  inputSchema: objectSchema({}),
20
20
  },
21
+ {
22
+ name: 'nubase_overview',
23
+ description: 'One-shot snapshot of the whole backend in a single call: capabilities, database schema, storage buckets, auth users, AI Gateway keys, current permissions, and suggested next steps. Call this first when starting a Nubase task. Read-only; each section degrades gracefully if unauthorized.',
24
+ inputSchema: objectSchema({
25
+ schema: { type: 'string' },
26
+ }),
27
+ },
21
28
  {
22
29
  name: 'memory_context',
23
30
  description: 'Return compact relevant memory context for a task. Scope defaults can come from NUBASE_USER_ID, NUBASE_AGENT_ID, and NUBASE_RUN_ID.',
@@ -69,6 +76,99 @@ export const TOOLS = [
69
76
  description: 'Execute SQL through Nubase admin API. Disabled unless NUBASE_ALLOW_SQL_EXECUTE=true.',
70
77
  inputSchema: objectSchema({ sql: { type: 'string' } }, ['sql']),
71
78
  },
79
+ {
80
+ name: 'db_export_schema',
81
+ description: 'Export table DDL for a Postgres schema (default public) to inspect the database structure. Read-only.',
82
+ inputSchema: objectSchema({
83
+ schema: { type: 'string' },
84
+ tables: { type: 'string' },
85
+ includeDrop: { type: 'boolean' },
86
+ }),
87
+ },
88
+ {
89
+ name: 'db_list_migrations',
90
+ description: 'List the audit trail of schema-changing SQL applied through sql_execute (most recent first), with timestamp, risk, and the SQL text. Read-only; returns an empty list if nothing has been recorded yet.',
91
+ inputSchema: objectSchema({
92
+ limit: { type: 'number' },
93
+ }),
94
+ },
95
+ {
96
+ name: 'storage_list_buckets',
97
+ description: 'List Nubase storage buckets. Read-only.',
98
+ inputSchema: objectSchema({
99
+ search: { type: 'string' },
100
+ limit: { type: 'number' },
101
+ offset: { type: 'number' },
102
+ }),
103
+ },
104
+ {
105
+ name: 'storage_create_bucket',
106
+ description: 'Create a storage bucket. Write op; disabled unless NUBASE_ALLOW_ADMIN_WRITE=true.',
107
+ inputSchema: objectSchema({
108
+ name: { type: 'string' },
109
+ public: { type: 'boolean' },
110
+ fileSizeLimit: { type: 'number' },
111
+ }, ['name']),
112
+ },
113
+ {
114
+ name: 'storage_delete_bucket',
115
+ description: 'Delete a storage bucket. Write op; disabled unless NUBASE_ALLOW_ADMIN_WRITE=true.',
116
+ inputSchema: objectSchema({ bucketId: { type: 'string' } }, ['bucketId']),
117
+ },
118
+ {
119
+ name: 'auth_list_users',
120
+ description: 'List auth users with optional keyword search. Read-only.',
121
+ inputSchema: objectSchema({
122
+ page: { type: 'number' },
123
+ perPage: { type: 'number' },
124
+ keyword: { type: 'string' },
125
+ }),
126
+ },
127
+ {
128
+ name: 'auth_create_user',
129
+ description: 'Create an auth user. Write op; disabled unless NUBASE_ALLOW_ADMIN_WRITE=true.',
130
+ inputSchema: objectSchema({
131
+ email: { type: 'string' },
132
+ password: { type: 'string' },
133
+ phone: { type: 'string' },
134
+ role: { type: 'string' },
135
+ }, ['email']),
136
+ },
137
+ {
138
+ name: 'auth_delete_user',
139
+ description: 'Delete an auth user by id. Write op; disabled unless NUBASE_ALLOW_ADMIN_WRITE=true.',
140
+ inputSchema: objectSchema({
141
+ userId: { type: 'string' },
142
+ softDelete: { type: 'boolean' },
143
+ }, ['userId']),
144
+ },
145
+ {
146
+ name: 'gateway_list_keys',
147
+ description: 'List AI Gateway self-routing keys (nbk_) for this project. Read-only.',
148
+ inputSchema: objectSchema({}),
149
+ },
150
+ {
151
+ name: 'gateway_issue_key',
152
+ description: 'Issue a new AI Gateway key (full key returned once). Write op; disabled unless NUBASE_ALLOW_ADMIN_WRITE=true.',
153
+ inputSchema: objectSchema({
154
+ name: { type: 'string' },
155
+ description: { type: 'string' },
156
+ expiresAt: { type: 'string' },
157
+ }),
158
+ },
159
+ {
160
+ name: 'gateway_revoke_key',
161
+ description: 'Revoke an AI Gateway key by id. Write op; disabled unless NUBASE_ALLOW_ADMIN_WRITE=true.',
162
+ inputSchema: objectSchema({ id: { type: 'string' } }, ['id']),
163
+ },
164
+ {
165
+ name: 'gateway_usage',
166
+ description: 'AI Gateway usage overview (tokens, requests, cost) for a date range. Read-only.',
167
+ inputSchema: objectSchema({
168
+ startDate: { type: 'string' },
169
+ endDate: { type: 'string' },
170
+ }),
171
+ },
72
172
  ];
73
173
  export async function callTool(name, args, config, client) {
74
174
  switch (name) {
@@ -78,6 +178,8 @@ export async function callTool(name, args, config, client) {
78
178
  return client.capabilities();
79
179
  case 'nubase_instructions':
80
180
  return client.instructions();
181
+ case 'nubase_overview':
182
+ return client.overview(args);
81
183
  case 'memory_context':
82
184
  return client.memoryContext(withScope(config, args));
83
185
  case 'memory_search':
@@ -90,6 +192,30 @@ export async function callTool(name, args, config, client) {
90
192
  return client.sqlDryRun(args);
91
193
  case 'sql_execute':
92
194
  return client.sqlExecute(args);
195
+ case 'db_export_schema':
196
+ return client.dbExportSchema(args);
197
+ case 'db_list_migrations':
198
+ return client.listMigrations(args);
199
+ case 'storage_list_buckets':
200
+ return client.storageListBuckets(args);
201
+ case 'storage_create_bucket':
202
+ return client.storageCreateBucket(args);
203
+ case 'storage_delete_bucket':
204
+ return client.storageDeleteBucket(args);
205
+ case 'auth_list_users':
206
+ return client.authListUsers(args);
207
+ case 'auth_create_user':
208
+ return client.authCreateUser(args);
209
+ case 'auth_delete_user':
210
+ return client.authDeleteUser(args);
211
+ case 'gateway_list_keys':
212
+ return client.gatewayListKeys();
213
+ case 'gateway_issue_key':
214
+ return client.gatewayIssueKey(args);
215
+ case 'gateway_revoke_key':
216
+ return client.gatewayRevokeKey(args);
217
+ case 'gateway_usage':
218
+ return client.gatewayUsage(args);
93
219
  default:
94
220
  throw new Error(`Unknown tool: ${name}`);
95
221
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nubase_cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,9 +20,9 @@ It provides:
20
20
 
21
21
  When starting a Nubase task:
22
22
 
23
- 1. Call `fetch_docs({ "topic": "overview" })` if the MCP tool is available.
23
+ 1. Call `nubase_overview()` first. One call returns the whole backend state — capabilities, database schema, storage buckets, auth users, AI Gateway keys, the permission gates that are on/off, and suggested next steps. Use it instead of separately calling `db_export_schema` + `storage_list_buckets` + `auth_list_users` + `gateway_list_keys`.
24
24
  2. Call `memory_context({ "task": "<current task>" })`.
25
- 3. Identify which capability owns the work and read the matching reference:
25
+ 3. Read `fetch_docs({ "topic": "overview" })` or a focused reference if you need detail. Identify which capability owns the work and read the matching reference:
26
26
  - `references/memory.md`
27
27
  - `references/database.md`
28
28
  - `references/auth-storage.md`
@@ -35,6 +35,9 @@ When starting a Nubase task:
35
35
 
36
36
  Expected tools from `nubase_cli`:
37
37
 
38
+ Core:
39
+
40
+ - `nubase_overview` (start here — one-shot backend snapshot)
38
41
  - `fetch_docs`
39
42
  - `nubase_capabilities`
40
43
  - `nubase_instructions`
@@ -45,6 +48,10 @@ Expected tools from `nubase_cli`:
45
48
  - `sql_dry_run`
46
49
  - `sql_execute`
47
50
 
51
+ Backend ops (read): `db_export_schema`, `db_list_migrations`, `storage_list_buckets`, `auth_list_users`, `gateway_list_keys`, `gateway_usage`.
52
+
53
+ Backend ops (write, gated by `NUBASE_ALLOW_ADMIN_WRITE=true`): `storage_create_bucket`, `storage_delete_bucket`, `auth_create_user`, `auth_delete_user`, `gateway_issue_key`, `gateway_revoke_key`. When the gate is off, these return `{ success: false, error }` without touching the backend. See `references/auth-storage.md` and `references/ai-gateway.md`.
54
+
48
55
  If a tool is unavailable, continue with REST/API guidance and tell the user what automation was unavailable.
49
56
 
50
57
  ## Setup
@@ -84,16 +91,7 @@ This installs one Nubase skill directory containing:
84
91
 
85
92
  ## Compatibility Language
86
93
 
87
- Say "Supabase-style" or "Supabase-compatible subset" unless exact SDK behavior is tested.
88
-
89
- Nubase targets common app-building compatibility for:
90
-
91
- - `/auth/v1`
92
- - `/rest/v1`
93
- - `/storage/v1`
94
- - `apikey` plus optional `Authorization: Bearer <jwt>`
95
-
96
- Do not claim complete Supabase Cloud replacement. Realtime, Edge Functions, and some SDK-specific edge cases may be absent.
94
+ `/auth/v1`, `/rest/v1`, and `/storage/v1` are Supabase-style compatible subsets (use `apikey` plus optional `Authorization: Bearer <jwt>`). Say "Supabase-style", not a complete Supabase Cloud replacement, unless exact SDK behavior is tested — Realtime, Edge Functions, and some SDK edge cases may be absent.
97
95
 
98
96
  ## Core Safety Rules
99
97
 
@@ -2,7 +2,23 @@
2
2
 
3
3
  Use this reference when configuring Nubase AI Gateway, model routing, OpenAI-compatible base URLs, Anthropic-compatible base URLs, gateway keys, usage logs, pricing, provider abstraction, or agent model configuration.
4
4
 
5
- AI Gateway is separate from MCP tools.
5
+ Model-call routing (the `/v1` surface below) is separate from MCP tools, but the gateway control plane is exposed as MCP tools.
6
+
7
+ ## Control-Plane Tools
8
+
9
+ Manage the project's gateway keys and usage without leaving the agent. These require the project key to carry `service_role`.
10
+
11
+ Read (always available):
12
+
13
+ - `gateway_list_keys()` — list this project's `nbk_` self-routing keys (no plaintext/secret returned)
14
+ - `gateway_usage({ startDate?, endDate? })` — tokens, requests, and cost overview for a date range
15
+
16
+ Write (gated by `NUBASE_ALLOW_ADMIN_WRITE=true`; otherwise returns `{ success: false, error }` without calling the backend):
17
+
18
+ - `gateway_issue_key({ name?, description?, expiresAt? })` — the full key is returned exactly once; treat it as a secret
19
+ - `gateway_revoke_key({ id })`
20
+
21
+ When `gateway_issue_key` returns a full key, never write it to Memory, generated code, or docs.
6
22
 
7
23
  Use AI Gateway for:
8
24
 
@@ -2,6 +2,26 @@
2
2
 
3
3
  Use this reference when implementing Nubase Auth, users, sessions, JWTs, Supabase-style `/auth/v1`, object storage, buckets, signed URLs, `/storage/v1`, uploads, downloads, or file metadata.
4
4
 
5
+ ## Admin Ops Tools
6
+
7
+ These tools run against admin endpoints and require the project key to carry `service_role`.
8
+
9
+ Read (always available):
10
+
11
+ - `auth_list_users({ page?, perPage?, keyword? })`
12
+ - `storage_list_buckets({ search?, limit?, offset? })`
13
+
14
+ Write (gated by `NUBASE_ALLOW_ADMIN_WRITE=true`; otherwise they return `{ success: false, error }` without calling the backend):
15
+
16
+ - `auth_create_user({ email, password?, phone?, role? })`
17
+ - `auth_delete_user({ userId, softDelete? })`
18
+ - `storage_create_bucket({ name, public?, fileSizeLimit? })`
19
+ - `storage_delete_bucket({ bucketId })`
20
+
21
+ Prefer the read tools for inspection. Before any write tool, confirm the user asked for it; bucket and user deletion are destructive and need explicit confirmation.
22
+
23
+ > Nubase has no serverless/edge-function runtime, so there are no function-deploy tools.
24
+
5
25
  ## Auth
6
26
 
7
27
  Auth base path:
@@ -20,6 +40,39 @@ Use Auth for:
20
40
 
21
41
  Generated frontend apps should use anon/authenticated keys plus user JWTs. Service-role keys must stay server-side or inside trusted local agent tooling.
22
42
 
43
+ ### Worked Example: signup → login → current user
44
+
45
+ ```http
46
+ POST /auth/v1/signup
47
+ apikey: <anon key>
48
+ Content-Type: application/json
49
+
50
+ { "email": "ada@example.com", "password": "s3cret-pass" }
51
+ ```
52
+
53
+ ```http
54
+ POST /auth/v1/token?grant_type=password
55
+ apikey: <anon key>
56
+ Content-Type: application/json
57
+
58
+ { "email": "ada@example.com", "password": "s3cret-pass" }
59
+ ```
60
+
61
+ Response carries the JWT the app stores and replays on every user-scoped request:
62
+
63
+ ```json
64
+ { "access_token": "<JWT>", "token_type": "bearer", "expires_in": 3600,
65
+ "refresh_token": "<refresh>", "user": { "id": "f3a...07", "email": "ada@example.com" } }
66
+ ```
67
+
68
+ ```http
69
+ GET /auth/v1/user
70
+ apikey: <anon key>
71
+ Authorization: Bearer <JWT>
72
+ ```
73
+
74
+ Refresh with `POST /auth/v1/token?grant_type=refresh_token` and body `{ "refresh_token": "<refresh>" }`.
75
+
23
76
  When implementing auth:
24
77
 
25
78
  1. Keep API base configurable.
@@ -44,6 +97,42 @@ Use Storage for:
44
97
  - signed URLs
45
98
  - resumable uploads when supported by the app flow
46
99
 
100
+ ### Worked Example: private upload via signed URL
101
+
102
+ ```text
103
+ storage_create_bucket({ "name": "avatars", "public": false }) # gated by NUBASE_ALLOW_ADMIN_WRITE
104
+ ```
105
+
106
+ Ask the backend for a one-time signed upload URL, then PUT the bytes to it:
107
+
108
+ ```http
109
+ POST /storage/v1/object/upload/sign/avatars/f3a...07/photo.png
110
+ apikey: <anon or authenticated key>
111
+ Authorization: Bearer <user JWT>
112
+ ```
113
+
114
+ ```json
115
+ { "signedUrl": "/storage/v1/object/upload/sign/avatars/...?token=<token>" }
116
+ ```
117
+
118
+ ```http
119
+ PUT <signedUrl>
120
+ Content-Type: image/png
121
+
122
+ <binary file bytes>
123
+ ```
124
+
125
+ Serve it back later with a short-lived signed download URL, and record the path in an app table:
126
+
127
+ ```http
128
+ POST /storage/v1/object/sign/avatars/f3a...07/photo.png Body: { "expiresIn": 3600 }
129
+ ```
130
+
131
+ ```text
132
+ # then persist the reference
133
+ POST /rest/v1/profiles Body: { "avatar_path": "avatars/f3a...07/photo.png" }
134
+ ```
135
+
47
136
  When generating file flows:
48
137
 
49
138
  1. Validate file size and MIME type.
@@ -52,6 +141,4 @@ When generating file flows:
52
141
  4. Avoid service_role in browser uploads.
53
142
  5. Store file references in app tables through `/rest/v1` when needed.
54
143
 
55
- ## Supabase Compatibility
56
-
57
- Say Supabase-style Auth/Storage or Supabase-compatible subset. Do not assume every Supabase SDK behavior exists unless the project has compatibility tests for it.
144
+ > `/auth/v1` and `/storage/v1` are Supabase-style subsets; don't assume every Supabase SDK behavior exists without a compatibility test.