@rebasepro/mcp 0.0.1-canary.4829d6e

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/dist/index.js ADDED
@@ -0,0 +1,1211 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
+ import { spawn } from "node:child_process";
5
+ import { config as loadDotenv } from "dotenv";
6
+ import { resolve } from "node:path";
7
+ import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ /**
10
+ * Detect the project's package manager by checking for lock files.
11
+ * Falls back to pnpm (Rebase's default) if no lock file is found.
12
+ */
13
+ export function detectPackageManager(projectDir) {
14
+ const candidates = [
15
+ ["pnpm-lock.yaml", "pnpm"],
16
+ ["pnpm-workspace.yaml", "pnpm"],
17
+ ["yarn.lock", "yarn"],
18
+ ["package-lock.json", "npm"]
19
+ ];
20
+ for (const [lockFile, pm] of candidates) {
21
+ if (existsSync(resolve(projectDir, lockFile)))
22
+ return pm;
23
+ }
24
+ // Also check in app/ subdirectory for scaffolded projects
25
+ for (const [lockFile, pm] of candidates) {
26
+ if (existsSync(resolve(projectDir, "app", lockFile)))
27
+ return pm;
28
+ }
29
+ return "pnpm"; // Rebase default
30
+ }
31
+ /** Return the exec command and its arguments prefix for running a package binary. */
32
+ export function getExecCommand(pm) {
33
+ switch (pm) {
34
+ case "pnpm":
35
+ return { command: "pnpm", args: ["exec"] };
36
+ case "yarn":
37
+ return { command: "yarn", args: ["exec"] };
38
+ case "npm":
39
+ return { command: "npx", args: [] };
40
+ }
41
+ }
42
+ /** Return the run command for executing package.json scripts. */
43
+ export function getRunCommand(pm) {
44
+ switch (pm) {
45
+ case "pnpm":
46
+ return { command: "pnpm", args: ["run"] };
47
+ case "yarn":
48
+ return { command: "yarn", args: ["run"] };
49
+ case "npm":
50
+ return { command: "npm", args: ["run"] };
51
+ }
52
+ }
53
+ // We dynamically load @rebasepro/client to avoid transitive type issues.
54
+ // The client SDK ships compiled .d.ts that reference @rebasepro/types (which
55
+ // drags in React peer-deps). By importing at runtime only we keep the MCP
56
+ // server build clean.
57
+ const CLIENT_PKG = "@rebasepro/client";
58
+ async function loadClientSdk() {
59
+ const mod = await import(/* webpackIgnore: true */ CLIENT_PKG);
60
+ return mod.createRebaseClient;
61
+ }
62
+ /** Path to the project registry file. */
63
+ const REGISTRY_PATH = resolve(homedir(), ".rebase", "projects.json");
64
+ /** In-memory project registry. */
65
+ let registry = { projects: {}, activeProject: null };
66
+ /**
67
+ * Load the project registry from disk. Creates the file if it doesn't exist.
68
+ */
69
+ function loadRegistry() {
70
+ try {
71
+ if (existsSync(REGISTRY_PATH)) {
72
+ const raw = readFileSync(REGISTRY_PATH, "utf-8");
73
+ const parsed = JSON.parse(raw);
74
+ return {
75
+ projects: parsed.projects || {},
76
+ activeProject: parsed.activeProject || null
77
+ };
78
+ }
79
+ }
80
+ catch {
81
+ // Corrupted file — start fresh
82
+ }
83
+ return { projects: {}, activeProject: null };
84
+ }
85
+ /**
86
+ * Save the project registry to disk.
87
+ */
88
+ function saveRegistry() {
89
+ try {
90
+ const dir = resolve(homedir(), ".rebase");
91
+ if (!existsSync(dir)) {
92
+ mkdirSync(dir, { recursive: true });
93
+ }
94
+ writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2), "utf-8");
95
+ }
96
+ catch {
97
+ // Non-fatal — registry won't persist across restarts
98
+ }
99
+ }
100
+ /**
101
+ * Read `.rebase/state.json` from a project directory to auto-discover
102
+ * a running dev server's URL and service key.
103
+ */
104
+ function readDevState(projectDir) {
105
+ try {
106
+ const statePath = resolve(projectDir, ".rebase", "state.json");
107
+ if (!existsSync(statePath))
108
+ return null;
109
+ const raw = readFileSync(statePath, "utf-8");
110
+ const state = JSON.parse(raw);
111
+ if (!state.baseUrl)
112
+ return null;
113
+ // Verify the process is still running (liveness check)
114
+ if (state.pid) {
115
+ try {
116
+ process.kill(state.pid, 0); // signal 0 = check existence
117
+ }
118
+ catch {
119
+ return null; // process is dead — stale state file
120
+ }
121
+ }
122
+ return {
123
+ baseUrl: state.baseUrl,
124
+ serviceKey: state.serviceKey,
125
+ pid: state.pid
126
+ };
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
132
+ /**
133
+ * Try to auto-discover the backend from `.rebase/state.json` in the project dir.
134
+ * Updates the project config in the registry if a running server is found.
135
+ */
136
+ function autoDiscoverLocal(project) {
137
+ if (!project.projectDir)
138
+ return project;
139
+ const devState = readDevState(project.projectDir);
140
+ if (devState) {
141
+ return {
142
+ ...project,
143
+ baseUrl: devState.baseUrl,
144
+ token: devState.serviceKey || project.token
145
+ };
146
+ }
147
+ return project;
148
+ }
149
+ /**
150
+ * Read `.env` from a project directory and extract REBASE_SERVICE_KEY.
151
+ */
152
+ function readServiceKeyFromEnv(projectDir) {
153
+ for (const envPath of [
154
+ resolve(projectDir, ".env"),
155
+ resolve(projectDir, "app", ".env")
156
+ ]) {
157
+ try {
158
+ if (!existsSync(envPath))
159
+ continue;
160
+ const content = readFileSync(envPath, "utf-8");
161
+ const match = content.match(/^REBASE_SERVICE_KEY\s*=\s*["']?([^"'\n\r]+)["']?/m);
162
+ if (match?.[1] && match[1].trim().length >= 32) {
163
+ return match[1].trim();
164
+ }
165
+ }
166
+ catch {
167
+ // ignore
168
+ }
169
+ }
170
+ return undefined;
171
+ }
172
+ // ── Environment & Initialization ────────────────────────────────────────────
173
+ const ENV_PROJECT_DIR = process.env.REBASE_PROJECT_DIR || process.cwd();
174
+ // Try to load .env from the project directory
175
+ for (const envPath of [
176
+ resolve(ENV_PROJECT_DIR, ".env"),
177
+ resolve(ENV_PROJECT_DIR, "app", ".env")
178
+ ]) {
179
+ if (existsSync(envPath)) {
180
+ loadDotenv({ path: envPath });
181
+ break;
182
+ }
183
+ }
184
+ const ENV_BASE_URL = process.env.REBASE_BASE_URL || "";
185
+ const ENV_API_TOKEN = process.env.REBASE_API_TOKEN || process.env.REBASE_TOKEN || "";
186
+ /**
187
+ * Initialize the project registry.
188
+ *
189
+ * Priority:
190
+ * 1. REBASE_PROJECT_DIR env → single-project mode (backward-compatible)
191
+ * 2. Load ~/.rebase/projects.json
192
+ * 3. Auto-discover from .rebase/state.json in the project dir
193
+ */
194
+ function initializeRegistry() {
195
+ registry = loadRegistry();
196
+ // Ensure a "default" project exists from env vars or CWD
197
+ if (!registry.projects["default"]) {
198
+ const devState = readDevState(ENV_PROJECT_DIR);
199
+ const envServiceKey = readServiceKeyFromEnv(ENV_PROJECT_DIR);
200
+ registry.projects["default"] = {
201
+ name: "default",
202
+ projectDir: ENV_PROJECT_DIR,
203
+ baseUrl: ENV_BASE_URL || devState?.baseUrl || "http://localhost:3001",
204
+ token: ENV_API_TOKEN || devState?.serviceKey || envServiceKey || "",
205
+ addedAt: new Date().toISOString()
206
+ };
207
+ }
208
+ if (!registry.activeProject || !registry.projects[registry.activeProject]) {
209
+ registry.activeProject = "default";
210
+ }
211
+ }
212
+ initializeRegistry();
213
+ /** Client instances keyed by project name. */
214
+ const clientCache = new Map();
215
+ /** Get the active project config, with auto-discovery applied. */
216
+ function getActiveProject() {
217
+ const name = registry.activeProject || "default";
218
+ const project = registry.projects[name];
219
+ if (!project) {
220
+ throw new Error(`No active project configured. Use rebase_project_add to register one.`);
221
+ }
222
+ return autoDiscoverLocal(project);
223
+ }
224
+ /** Get the project directory for the active project. */
225
+ function getProjectDir() {
226
+ const project = getActiveProject();
227
+ return project.projectDir || ENV_PROJECT_DIR;
228
+ }
229
+ async function getClient() {
230
+ const project = getActiveProject();
231
+ const cacheKey = `${project.name}::${project.baseUrl}::${project.token}`;
232
+ const cached = clientCache.get(cacheKey);
233
+ if (cached)
234
+ return cached;
235
+ const createRebaseClient = await loadClientSdk();
236
+ const client = createRebaseClient({
237
+ baseUrl: project.baseUrl,
238
+ token: project.token || undefined
239
+ });
240
+ clientCache.set(cacheKey, client);
241
+ return client;
242
+ }
243
+ /** Clear cached clients (used when switching projects). */
244
+ function clearClientCache() {
245
+ clientCache.clear();
246
+ }
247
+ async function ensureAdmin() {
248
+ const client = await getClient();
249
+ try {
250
+ const user = await client.auth.getUser();
251
+ if (!user.roles?.includes("admin")) {
252
+ throw new Error("Access denied: User does not have the 'admin' role.");
253
+ }
254
+ }
255
+ catch (err) {
256
+ const msg = err instanceof Error ? err.message : String(err);
257
+ throw new Error(`Admin authorization failed: ${msg}`);
258
+ }
259
+ }
260
+ // ── MCP Server ──────────────────────────────────────────────────────────────
261
+ export const server = new Server({ name: "rebase-mcp-server",
262
+ version: "0.1.0" }, { capabilities: { tools: {},
263
+ resources: {} } });
264
+ const CLI_TOOLS = [
265
+ {
266
+ name: "rebase_schema_generate",
267
+ description: "Generate Drizzle schema from Rebase TypeScript collection definitions. Run this after adding or modifying collection files.",
268
+ inputSchema: { type: "object",
269
+ properties: {} },
270
+ cmd: ["schema", "generate"]
271
+ },
272
+ {
273
+ name: "rebase_db_push",
274
+ description: "Apply the current Drizzle schema directly to the database (development shortcut, skips migration files).",
275
+ inputSchema: { type: "object",
276
+ properties: {} },
277
+ cmd: ["db", "push"]
278
+ },
279
+ {
280
+ name: "rebase_schema_introspect",
281
+ description: "Introspect the live database and generate Rebase collection definitions from existing tables.",
282
+ inputSchema: { type: "object",
283
+ properties: {} },
284
+ cmd: ["schema", "introspect"]
285
+ },
286
+ {
287
+ name: "rebase_db_generate",
288
+ description: "Generate SQL migration files from schema changes (compares current Drizzle schema against the last entity).",
289
+ inputSchema: { type: "object",
290
+ properties: {} },
291
+ cmd: ["db", "generate"]
292
+ },
293
+ {
294
+ name: "rebase_db_migrate",
295
+ description: "Run all pending SQL migrations against the database.",
296
+ inputSchema: { type: "object",
297
+ properties: {} },
298
+ cmd: ["db", "migrate"]
299
+ },
300
+ {
301
+ name: "rebase_generate_sdk",
302
+ description: "Generate a fully-typed JavaScript/TypeScript SDK from collection definitions.",
303
+ inputSchema: { type: "object",
304
+ properties: {} },
305
+ cmd: ["generate-sdk"]
306
+ },
307
+ {
308
+ name: "rebase_doctor",
309
+ description: "Detect schema drift between collection definitions, generated Drizzle schema, and the live PostgreSQL database.",
310
+ inputSchema: { type: "object", properties: {} },
311
+ cmd: ["doctor"]
312
+ },
313
+ {
314
+ name: "rebase_db_branch_create",
315
+ description: "Create a new database branch (Admins only).",
316
+ inputSchema: {
317
+ type: "object",
318
+ properties: {
319
+ name: { type: "string", description: "Name of the new database branch" },
320
+ from: { type: "string", description: "Parent branch to clone from (optional)" }
321
+ },
322
+ required: ["name"]
323
+ },
324
+ cmd: ["db", "branch", "create"]
325
+ },
326
+ {
327
+ name: "rebase_db_branch_list",
328
+ description: "List all database branches (Admins only).",
329
+ inputSchema: { type: "object", properties: {} },
330
+ cmd: ["db", "branch", "list"]
331
+ },
332
+ {
333
+ name: "rebase_db_branch_delete",
334
+ description: "Delete an existing database branch (Admins only).",
335
+ inputSchema: {
336
+ type: "object",
337
+ properties: {
338
+ name: { type: "string", description: "Name of the branch to delete" }
339
+ },
340
+ required: ["name"]
341
+ },
342
+ cmd: ["db", "branch", "delete"]
343
+ },
344
+ {
345
+ name: "rebase_db_branch_info",
346
+ description: "Show information and status for a database branch (Admins only).",
347
+ inputSchema: {
348
+ type: "object",
349
+ properties: {
350
+ name: { type: "string", description: "Name of the branch to inspect" }
351
+ },
352
+ required: ["name"]
353
+ },
354
+ cmd: ["db", "branch", "info"]
355
+ }
356
+ ];
357
+ const DATA_TOOLS = [
358
+ {
359
+ name: "list_documents",
360
+ description: "List documents from a Rebase collection with optional filtering, sorting, and pagination.",
361
+ inputSchema: {
362
+ type: "object",
363
+ properties: {
364
+ collection: { type: "string",
365
+ description: "Collection slug" },
366
+ limit: { type: "number",
367
+ description: "Max results (default 25)" },
368
+ offset: { type: "number",
369
+ description: "Skip N results" },
370
+ orderBy: { type: "string",
371
+ description: "Sort field, optionally with :asc or :desc suffix" },
372
+ where: {
373
+ type: "object",
374
+ description: "Filter object, e.g. { \"status\": \"eq.active\", \"price\": \"gte.100\" }",
375
+ additionalProperties: true
376
+ }
377
+ },
378
+ required: ["collection"]
379
+ }
380
+ },
381
+ {
382
+ name: "get_document",
383
+ description: "Get a single document by ID from a Rebase collection.",
384
+ inputSchema: {
385
+ type: "object",
386
+ properties: {
387
+ collection: { type: "string",
388
+ description: "Collection slug" },
389
+ id: { type: "string",
390
+ description: "Document ID" }
391
+ },
392
+ required: ["collection", "id"]
393
+ }
394
+ },
395
+ {
396
+ name: "create_document",
397
+ description: "Create a new document in a Rebase collection.",
398
+ inputSchema: {
399
+ type: "object",
400
+ properties: {
401
+ collection: { type: "string",
402
+ description: "Collection slug" },
403
+ data: { type: "object",
404
+ description: "Document data",
405
+ additionalProperties: true }
406
+ },
407
+ required: ["collection", "data"]
408
+ }
409
+ },
410
+ {
411
+ name: "update_document",
412
+ description: "Update an existing document in a Rebase collection.",
413
+ inputSchema: {
414
+ type: "object",
415
+ properties: {
416
+ collection: { type: "string",
417
+ description: "Collection slug" },
418
+ id: { type: "string",
419
+ description: "Document ID" },
420
+ data: { type: "object",
421
+ description: "Fields to update",
422
+ additionalProperties: true }
423
+ },
424
+ required: ["collection", "id", "data"]
425
+ }
426
+ },
427
+ {
428
+ name: "delete_document",
429
+ description: "Delete a document from a Rebase collection.",
430
+ inputSchema: {
431
+ type: "object",
432
+ properties: {
433
+ collection: { type: "string",
434
+ description: "Collection slug" },
435
+ id: { type: "string",
436
+ description: "Document ID" }
437
+ },
438
+ required: ["collection", "id"]
439
+ }
440
+ }
441
+ ];
442
+ const ADMIN_TOOLS = [
443
+ {
444
+ name: "list_users",
445
+ description: "List all users registered in the Rebase backend, including their roles.",
446
+ inputSchema: { type: "object",
447
+ properties: {} }
448
+ },
449
+ {
450
+ name: "create_user",
451
+ description: "Create a new user in the Rebase backend.",
452
+ inputSchema: {
453
+ type: "object",
454
+ properties: {
455
+ email: { type: "string",
456
+ description: "User email" },
457
+ displayName: { type: "string",
458
+ description: "Display name" },
459
+ password: { type: "string",
460
+ description: "Initial password" },
461
+ roles: { type: "array",
462
+ items: { type: "string" },
463
+ description: "Role IDs to assign" }
464
+ },
465
+ required: ["email"]
466
+ }
467
+ },
468
+ {
469
+ name: "update_user",
470
+ description: "Update an existing user (email, display name, roles).",
471
+ inputSchema: {
472
+ type: "object",
473
+ properties: {
474
+ userId: { type: "string",
475
+ description: "User UID" },
476
+ email: { type: "string" },
477
+ displayName: { type: "string" },
478
+ roles: { type: "array",
479
+ items: { type: "string" } }
480
+ },
481
+ required: ["userId"]
482
+ }
483
+ },
484
+ {
485
+ name: "delete_user",
486
+ description: "Delete a user from the Rebase backend.",
487
+ inputSchema: {
488
+ type: "object",
489
+ properties: {
490
+ userId: { type: "string",
491
+ description: "User UID" }
492
+ },
493
+ required: ["userId"]
494
+ }
495
+ },
496
+ {
497
+ name: "list_roles",
498
+ description: "List all roles defined in the Rebase backend.",
499
+ inputSchema: { type: "object",
500
+ properties: {} }
501
+ },
502
+ {
503
+ name: "rebase_auth_reset_password",
504
+ description: "Reset a user's password via the admin API. Looks up the user by email, then resets their password. Returns a temporary password if email is not configured, or sends a reset email.",
505
+ inputSchema: {
506
+ type: "object",
507
+ properties: {
508
+ email: { type: "string", description: "Email of the user to reset" },
509
+ password: { type: "string", description: "New password to set (optional — if omitted, a secure temporary password is generated)" }
510
+ },
511
+ required: ["email"]
512
+ }
513
+ }
514
+ ];
515
+ const DEV_TOOLS = [
516
+ {
517
+ name: "rebase_dev_start",
518
+ description: "Start the Rebase development server (frontend + backend). Returns immediately — use rebase_dev_logs to check output.",
519
+ inputSchema: { type: "object",
520
+ properties: {} }
521
+ },
522
+ {
523
+ name: "rebase_dev_logs",
524
+ description: "Read recent output from the running Rebase dev server.",
525
+ inputSchema: {
526
+ type: "object",
527
+ properties: {
528
+ lines: { type: "number",
529
+ description: "Number of recent lines to return (default 50)" }
530
+ }
531
+ }
532
+ },
533
+ {
534
+ name: "rebase_dev_stop",
535
+ description: "Stop the running Rebase development server.",
536
+ inputSchema: { type: "object",
537
+ properties: {} }
538
+ }
539
+ ];
540
+ const STORAGE_TOOLS = [
541
+ {
542
+ name: "storage_list_objects",
543
+ description: "List files/objects stored in Rebase storage.",
544
+ inputSchema: {
545
+ type: "object",
546
+ properties: {
547
+ prefix: { type: "string", description: "Filter objects by prefix (e.g. 'images/')" },
548
+ bucket: { type: "string", description: "Filter by storage bucket name" },
549
+ maxResults: { type: "number", description: "Maximum number of results to return (default 50)" },
550
+ pageToken: { type: "string", description: "Pagination token" }
551
+ }
552
+ }
553
+ },
554
+ {
555
+ name: "storage_delete_object",
556
+ description: "Delete an object/file from Rebase storage.",
557
+ inputSchema: {
558
+ type: "object",
559
+ properties: {
560
+ key: { type: "string", description: "Key/path of the file to delete (e.g., 'images/profile.png')" },
561
+ bucket: { type: "string", description: "Storage bucket name" }
562
+ },
563
+ required: ["key"]
564
+ }
565
+ },
566
+ {
567
+ name: "storage_get_metadata",
568
+ description: "Get metadata and a temporary signed download URL for a file in Rebase storage.",
569
+ inputSchema: {
570
+ type: "object",
571
+ properties: {
572
+ key: { type: "string", description: "Key/path/url of the file to download" },
573
+ bucket: { type: "string", description: "Storage bucket name" }
574
+ },
575
+ required: ["key"]
576
+ }
577
+ }
578
+ ];
579
+ const CRON_TOOLS = [
580
+ {
581
+ name: "cron_list_jobs",
582
+ description: "List all scheduled cron jobs and their configuration status.",
583
+ inputSchema: { type: "object", properties: {} }
584
+ },
585
+ {
586
+ name: "cron_get_job",
587
+ description: "Get status and details of a specific scheduled cron job.",
588
+ inputSchema: {
589
+ type: "object",
590
+ properties: {
591
+ jobId: { type: "string", description: "Unique identifier of the cron job" }
592
+ },
593
+ required: ["jobId"]
594
+ }
595
+ },
596
+ {
597
+ name: "cron_trigger_job",
598
+ description: "Manually trigger a cron job run immediately.",
599
+ inputSchema: {
600
+ type: "object",
601
+ properties: {
602
+ jobId: { type: "string", description: "Unique identifier of the cron job to run" }
603
+ },
604
+ required: ["jobId"]
605
+ }
606
+ },
607
+ {
608
+ name: "cron_get_job_logs",
609
+ description: "Read execution logs for a specific cron job.",
610
+ inputSchema: {
611
+ type: "object",
612
+ properties: {
613
+ jobId: { type: "string", description: "Unique identifier of the cron job" },
614
+ limit: { type: "number", description: "Number of log lines to return (default 50)" }
615
+ },
616
+ required: ["jobId"]
617
+ }
618
+ },
619
+ {
620
+ name: "cron_toggle_job",
621
+ description: "Enable or disable a scheduled cron job.",
622
+ inputSchema: {
623
+ type: "object",
624
+ properties: {
625
+ jobId: { type: "string", description: "Unique identifier of the cron job" },
626
+ enabled: { type: "boolean", description: "Set to true to enable, false to disable" }
627
+ },
628
+ required: ["jobId", "enabled"]
629
+ }
630
+ }
631
+ ];
632
+ const FUNCTION_TOOLS = [
633
+ {
634
+ name: "invoke_function",
635
+ description: "Invoke a custom backend Hono function (located in api/functions/:name).",
636
+ inputSchema: {
637
+ type: "object",
638
+ properties: {
639
+ name: { type: "string", description: "Function name (filename without extension, e.g. 'send-welcome-email')" },
640
+ payload: { type: "object", description: "Optional JSON payload body for POST/PUT/PATCH requests", additionalProperties: true },
641
+ method: { type: "string", enum: ["GET", "POST", "PUT", "PATCH", "DELETE"], description: "HTTP Method (defaults to POST)" },
642
+ path: { type: "string", description: "Optional sub-path to append after the function name (e.g. 'status/123')" }
643
+ },
644
+ required: ["name"]
645
+ }
646
+ }
647
+ ];
648
+ const PROJECT_TOOLS = [
649
+ {
650
+ name: "rebase_project_list",
651
+ description: "List all registered Rebase projects and show which one is active.",
652
+ inputSchema: { type: "object", properties: {} }
653
+ },
654
+ {
655
+ name: "rebase_project_switch",
656
+ description: "Switch the active Rebase project by name. All subsequent API calls will target this project.",
657
+ inputSchema: {
658
+ type: "object",
659
+ properties: {
660
+ name: { type: "string", description: "Name of the project to switch to" }
661
+ },
662
+ required: ["name"]
663
+ }
664
+ },
665
+ {
666
+ name: "rebase_project_add",
667
+ description: "Register a new Rebase project. For local projects, provide projectDir (auto-discovers URL and service key). For remote projects, provide baseUrl and token.",
668
+ inputSchema: {
669
+ type: "object",
670
+ properties: {
671
+ name: { type: "string", description: "Unique name for this project (e.g. 'my-app', 'staging')" },
672
+ projectDir: { type: "string", description: "Absolute path to the project directory (for local projects)" },
673
+ baseUrl: { type: "string", description: "Backend URL (e.g. https://staging.myapp.com)" },
674
+ token: { type: "string", description: "Auth token — service key or API key (for remote projects)" }
675
+ },
676
+ required: ["name"]
677
+ }
678
+ },
679
+ {
680
+ name: "rebase_project_remove",
681
+ description: "Remove a registered project from the project registry.",
682
+ inputSchema: {
683
+ type: "object",
684
+ properties: {
685
+ name: { type: "string", description: "Name of the project to remove" }
686
+ },
687
+ required: ["name"]
688
+ }
689
+ },
690
+ {
691
+ name: "rebase_project_current",
692
+ description: "Show details about the currently active Rebase project, including resolved URL and auth status.",
693
+ inputSchema: { type: "object", properties: {} }
694
+ },
695
+ {
696
+ name: "rebase_project_status",
697
+ description: "Health-check the active project's backend by calling GET /health.",
698
+ inputSchema: { type: "object", properties: {} }
699
+ }
700
+ ];
701
+ export const ALL_TOOLS = [
702
+ ...CLI_TOOLS.map(({ cmd: _c, ...rest }) => rest),
703
+ ...DATA_TOOLS,
704
+ ...ADMIN_TOOLS,
705
+ ...DEV_TOOLS,
706
+ ...STORAGE_TOOLS,
707
+ ...CRON_TOOLS,
708
+ ...FUNCTION_TOOLS,
709
+ ...PROJECT_TOOLS
710
+ ];
711
+ // ── Tool Handlers ───────────────────────────────────────────────────────────
712
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
713
+ tools: ALL_TOOLS
714
+ }));
715
+ /** Spawn the rebase CLI using the project's detected package manager. */
716
+ function runRebaseCmd(commandArgs) {
717
+ const projectDir = getProjectDir();
718
+ const pm = detectPackageManager(projectDir);
719
+ const { command, args: execArgs } = getExecCommand(pm);
720
+ return new Promise((resolve) => {
721
+ const child = spawn(command, [...execArgs, "rebase", ...commandArgs], {
722
+ cwd: projectDir,
723
+ shell: true,
724
+ env: {
725
+ ...process.env,
726
+ PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: "false"
727
+ }
728
+ });
729
+ const chunks = [];
730
+ child.stdout?.on("data", (d) => chunks.push(d.toString()));
731
+ child.stderr?.on("data", (d) => chunks.push(d.toString()));
732
+ child.on("error", (err) => resolve(`Error spawning command: ${err.message}`));
733
+ child.on("close", (code) => {
734
+ const output = chunks.join("").trim();
735
+ resolve(code !== 0 ? `Command exited with code ${code}\n\n${output}` : output || "(no output)");
736
+ });
737
+ });
738
+ }
739
+ // Dev server management
740
+ let devProcess = null;
741
+ const devLogs = [];
742
+ const MAX_DEV_LOG_LINES = 500;
743
+ function appendDevLog(line) {
744
+ devLogs.push(line);
745
+ if (devLogs.length > MAX_DEV_LOG_LINES) {
746
+ devLogs.splice(0, devLogs.length - MAX_DEV_LOG_LINES);
747
+ }
748
+ }
749
+ function textResult(text) {
750
+ return { content: [{ type: "text",
751
+ text }] };
752
+ }
753
+ function jsonResult(data) {
754
+ return textResult(JSON.stringify(data, null, 2));
755
+ }
756
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
757
+ try {
758
+ const { name, arguments: args } = request.params;
759
+ // ── CLI tools ───────────────────────────────────────────────────────
760
+ const cliTool = CLI_TOOLS.find((t) => t.name === name);
761
+ if (cliTool) {
762
+ if (name.startsWith("rebase_db_branch_")) {
763
+ await ensureAdmin();
764
+ }
765
+ const cmdArgs = [...cliTool.cmd];
766
+ if (name === "rebase_db_branch_create") {
767
+ const argsObj = args;
768
+ cmdArgs.push(argsObj.name);
769
+ if (argsObj.from) {
770
+ cmdArgs.push("--from", argsObj.from);
771
+ }
772
+ }
773
+ else if (name === "rebase_db_branch_delete" || name === "rebase_db_branch_info") {
774
+ const argsObj = args;
775
+ cmdArgs.push(argsObj.name);
776
+ }
777
+ const result = await runRebaseCmd(cmdArgs);
778
+ return textResult(result);
779
+ }
780
+ // ── Project management tools ────────────────────────────────────────
781
+ switch (name) {
782
+ case "rebase_project_list": {
783
+ const projects = Object.values(registry.projects).map((p) => ({
784
+ name: p.name,
785
+ projectDir: p.projectDir || null,
786
+ baseUrl: p.baseUrl,
787
+ hasToken: !!p.token,
788
+ active: p.name === registry.activeProject,
789
+ addedAt: p.addedAt
790
+ }));
791
+ return jsonResult({ projects, activeProject: registry.activeProject });
792
+ }
793
+ case "rebase_project_switch": {
794
+ const argsObj = args;
795
+ if (!registry.projects[argsObj.name]) {
796
+ return textResult(`Project "${argsObj.name}" not found. Available: ${Object.keys(registry.projects).join(", ")}`);
797
+ }
798
+ registry.activeProject = argsObj.name;
799
+ clearClientCache();
800
+ saveRegistry();
801
+ const project = getActiveProject();
802
+ return jsonResult({
803
+ message: `Switched to project "${argsObj.name}"`,
804
+ project: {
805
+ name: project.name,
806
+ baseUrl: project.baseUrl,
807
+ hasToken: !!project.token,
808
+ projectDir: project.projectDir || null
809
+ }
810
+ });
811
+ }
812
+ case "rebase_project_add": {
813
+ const argsObj = args;
814
+ const { name: projectName } = argsObj;
815
+ let baseUrl = argsObj.baseUrl || "";
816
+ let token = argsObj.token || "";
817
+ // Auto-discover from project dir if provided
818
+ if (argsObj.projectDir) {
819
+ const devState = readDevState(argsObj.projectDir);
820
+ if (devState) {
821
+ baseUrl = baseUrl || devState.baseUrl;
822
+ token = token || devState.serviceKey || "";
823
+ }
824
+ if (!token) {
825
+ const envKey = readServiceKeyFromEnv(argsObj.projectDir);
826
+ if (envKey)
827
+ token = envKey;
828
+ }
829
+ }
830
+ if (!baseUrl) {
831
+ return textResult("Error: Could not determine baseUrl. Provide --baseUrl or ensure the dev server is running in the project directory.");
832
+ }
833
+ registry.projects[projectName] = {
834
+ name: projectName,
835
+ projectDir: argsObj.projectDir,
836
+ baseUrl,
837
+ token,
838
+ addedAt: new Date().toISOString()
839
+ };
840
+ saveRegistry();
841
+ return jsonResult({
842
+ message: `Project "${projectName}" registered`,
843
+ project: {
844
+ name: projectName,
845
+ baseUrl,
846
+ hasToken: !!token,
847
+ projectDir: argsObj.projectDir || null
848
+ }
849
+ });
850
+ }
851
+ case "rebase_project_remove": {
852
+ const argsObj = args;
853
+ if (argsObj.name === "default") {
854
+ return textResult("Cannot remove the default project.");
855
+ }
856
+ if (!registry.projects[argsObj.name]) {
857
+ return textResult(`Project "${argsObj.name}" not found.`);
858
+ }
859
+ delete registry.projects[argsObj.name];
860
+ if (registry.activeProject === argsObj.name) {
861
+ registry.activeProject = "default";
862
+ clearClientCache();
863
+ }
864
+ saveRegistry();
865
+ return textResult(`Project "${argsObj.name}" removed.`);
866
+ }
867
+ case "rebase_project_current": {
868
+ const project = getActiveProject();
869
+ return jsonResult({
870
+ name: project.name,
871
+ projectDir: project.projectDir || null,
872
+ baseUrl: project.baseUrl,
873
+ hasToken: !!project.token,
874
+ tokenPrefix: project.token ? project.token.substring(0, 8) + "..." : null,
875
+ addedAt: project.addedAt
876
+ });
877
+ }
878
+ case "rebase_project_status": {
879
+ const project = getActiveProject();
880
+ try {
881
+ // Try `/health` first (standard Rebase backend), fall back to `/api/health` if it 404s
882
+ let res = await fetch(`${project.baseUrl}/health`);
883
+ if (res.status === 404) {
884
+ const fallbackRes = await fetch(`${project.baseUrl}/api/health`);
885
+ if (fallbackRes.status !== 404) {
886
+ res = fallbackRes;
887
+ }
888
+ }
889
+ let body = {};
890
+ const contentType = res.headers.get("content-type");
891
+ if (contentType && contentType.includes("application/json")) {
892
+ try {
893
+ body = await res.json();
894
+ }
895
+ catch {
896
+ // ignore
897
+ }
898
+ }
899
+ else {
900
+ body = { responseText: await res.text().catch(() => "") };
901
+ }
902
+ return jsonResult({
903
+ project: project.name,
904
+ baseUrl: project.baseUrl,
905
+ status: res.ok ? "healthy" : "unhealthy",
906
+ httpStatus: res.status,
907
+ ...body
908
+ });
909
+ }
910
+ catch (err) {
911
+ const msg = err instanceof Error ? err.message : String(err);
912
+ return jsonResult({
913
+ project: project.name,
914
+ baseUrl: project.baseUrl,
915
+ status: "unreachable",
916
+ error: msg
917
+ });
918
+ }
919
+ }
920
+ }
921
+ // ── Data & admin tools (via @rebasepro/client) ──────────────────────
922
+ const client = await getClient();
923
+ switch (name) {
924
+ case "list_documents": {
925
+ const argsObj = args;
926
+ const { collection: slug, limit, offset, orderBy, where } = argsObj;
927
+ const result = await client.data.collection(slug).find({
928
+ limit,
929
+ offset,
930
+ orderBy,
931
+ where
932
+ });
933
+ return jsonResult(result);
934
+ }
935
+ case "get_document": {
936
+ const argsObj = args;
937
+ const { collection: slug, id } = argsObj;
938
+ const entity = await client.data.collection(slug).findById(id);
939
+ if (!entity)
940
+ return textResult(`Document ${id} not found in ${slug}`);
941
+ return jsonResult(entity);
942
+ }
943
+ case "create_document": {
944
+ const argsObj = args;
945
+ const { collection: slug, data } = argsObj;
946
+ const entity = await client.data.collection(slug).create(data);
947
+ return jsonResult(entity);
948
+ }
949
+ case "update_document": {
950
+ const argsObj = args;
951
+ const { collection: slug, id, data } = argsObj;
952
+ const entity = await client.data.collection(slug).update(id, data);
953
+ return jsonResult(entity);
954
+ }
955
+ case "delete_document": {
956
+ const argsObj = args;
957
+ const { collection: slug, id } = argsObj;
958
+ await client.data.collection(slug).delete(id);
959
+ return textResult(`Deleted document ${id} from ${slug}`);
960
+ }
961
+ // ── Admin tools ────────────────────────────────────────────────────
962
+ case "list_users": {
963
+ const result = await client.admin.listUsers();
964
+ return jsonResult(result);
965
+ }
966
+ case "create_user": {
967
+ const argsObj = args;
968
+ const { email, displayName, password, roles } = argsObj;
969
+ const result = await client.admin.createUser({ email,
970
+ displayName,
971
+ password,
972
+ roles });
973
+ return jsonResult(result);
974
+ }
975
+ case "update_user": {
976
+ const argsObj = args;
977
+ const { userId, email, displayName, roles } = argsObj;
978
+ const result = await client.admin.updateUser(userId, { email,
979
+ displayName,
980
+ roles });
981
+ return jsonResult(result);
982
+ }
983
+ case "delete_user": {
984
+ const argsObj = args;
985
+ const { userId } = argsObj;
986
+ const result = await client.admin.deleteUser(userId);
987
+ return jsonResult(result);
988
+ }
989
+ case "list_roles": {
990
+ const result = await client.admin.listRoles();
991
+ return jsonResult(result);
992
+ }
993
+ case "rebase_auth_reset_password": {
994
+ const argsObj = args;
995
+ const { email, password } = argsObj;
996
+ // Step 1: Find user by email
997
+ const usersResult = await client.admin.listUsersPaginated({ search: email, limit: 1 });
998
+ const user = usersResult.users.find((u) => u.email === email);
999
+ if (!user) {
1000
+ return textResult(`User with email "${email}" not found.`);
1001
+ }
1002
+ const userId = user.uid || user.id;
1003
+ if (!userId) {
1004
+ return textResult(`Could not determine user ID for "${email}".`);
1005
+ }
1006
+ // Step 2: Reset password via admin API
1007
+ const resetResult = await client.admin.resetPassword(userId, password ? { password } : undefined);
1008
+ return jsonResult({
1009
+ message: `Password reset for ${email}`,
1010
+ user: resetResult.user,
1011
+ temporaryPassword: resetResult.temporaryPassword,
1012
+ invitationSent: resetResult.invitationSent
1013
+ });
1014
+ }
1015
+ // ── Storage Tools ──────────────────────────────────────────────────
1016
+ case "storage_list_objects": {
1017
+ const argsObj = args;
1018
+ const { prefix = "", bucket, maxResults, pageToken } = argsObj;
1019
+ const result = await client.storage.listObjects(prefix, { bucket, maxResults, pageToken });
1020
+ return jsonResult(result);
1021
+ }
1022
+ case "storage_delete_object": {
1023
+ const argsObj = args;
1024
+ const { key, bucket } = argsObj;
1025
+ await client.storage.deleteObject(key, bucket);
1026
+ return textResult(`Deleted object "${key}" successfully.`);
1027
+ }
1028
+ case "storage_get_metadata": {
1029
+ const argsObj = args;
1030
+ const { key, bucket } = argsObj;
1031
+ const result = await client.storage.getSignedUrl(key, bucket);
1032
+ return jsonResult(result);
1033
+ }
1034
+ // ── Cron Tools ─────────────────────────────────────────────────────
1035
+ case "cron_list_jobs": {
1036
+ const result = await client.cron.listJobs();
1037
+ return jsonResult(result);
1038
+ }
1039
+ case "cron_get_job": {
1040
+ const argsObj = args;
1041
+ const result = await client.cron.getJob(argsObj.jobId);
1042
+ return jsonResult(result);
1043
+ }
1044
+ case "cron_trigger_job": {
1045
+ const argsObj = args;
1046
+ const result = await client.cron.triggerJob(argsObj.jobId);
1047
+ return jsonResult(result);
1048
+ }
1049
+ case "cron_get_job_logs": {
1050
+ const argsObj = args;
1051
+ const result = await client.cron.getJobLogs(argsObj.jobId, { limit: argsObj.limit });
1052
+ return jsonResult(result);
1053
+ }
1054
+ case "cron_toggle_job": {
1055
+ const argsObj = args;
1056
+ const result = await client.cron.toggleJob(argsObj.jobId, argsObj.enabled);
1057
+ return jsonResult(result);
1058
+ }
1059
+ // ── Function Tools ─────────────────────────────────────────────────
1060
+ case "invoke_function": {
1061
+ const argsObj = args;
1062
+ const { name: funcName, payload, method, path: funcPath } = argsObj;
1063
+ const result = await client.functions.invoke(funcName, payload, { method, path: funcPath });
1064
+ return jsonResult(result);
1065
+ }
1066
+ // ── Dev server management ──────────────────────────────────────────
1067
+ case "rebase_dev_start": {
1068
+ if (devProcess && !devProcess.killed) {
1069
+ return textResult("Dev server is already running (PID " + devProcess.pid + ")");
1070
+ }
1071
+ devLogs.length = 0;
1072
+ const projectDir = getProjectDir();
1073
+ const pm = detectPackageManager(projectDir);
1074
+ const { command: runCmd, args: runArgs } = getRunCommand(pm);
1075
+ devProcess = spawn(runCmd, [...runArgs, "dev"], {
1076
+ cwd: resolve(projectDir, "app"),
1077
+ shell: true,
1078
+ env: { ...process.env }
1079
+ });
1080
+ devProcess.stdout?.on("data", (d) => appendDevLog(d.toString()));
1081
+ devProcess.stderr?.on("data", (d) => appendDevLog(d.toString()));
1082
+ devProcess.on("close", (code) => {
1083
+ appendDevLog(`\n[dev server exited with code ${code}]`);
1084
+ devProcess = null;
1085
+ });
1086
+ // Wait a moment for initial output
1087
+ await new Promise((r) => setTimeout(r, 2000));
1088
+ return textResult(`Dev server started (PID ${devProcess?.pid})\n\n${devLogs.join("")}`);
1089
+ }
1090
+ case "rebase_dev_logs": {
1091
+ const argsObj = args;
1092
+ const lineCount = argsObj?.lines ?? 50;
1093
+ const recent = devLogs.slice(-lineCount);
1094
+ if (recent.length === 0) {
1095
+ return textResult(devProcess ? "No output captured yet." : "Dev server is not running.");
1096
+ }
1097
+ return textResult(recent.join(""));
1098
+ }
1099
+ case "rebase_dev_stop": {
1100
+ if (!devProcess || devProcess.killed) {
1101
+ return textResult("Dev server is not running.");
1102
+ }
1103
+ devProcess.kill("SIGTERM");
1104
+ return textResult("Dev server stopped.");
1105
+ }
1106
+ default:
1107
+ throw new Error(`Unknown tool: ${name}`);
1108
+ }
1109
+ }
1110
+ catch (err) {
1111
+ const msg = err instanceof Error ? err.message : String(err);
1112
+ return {
1113
+ content: [{
1114
+ type: "text",
1115
+ text: `Error: ${msg}`
1116
+ }],
1117
+ isError: true
1118
+ };
1119
+ }
1120
+ });
1121
+ // ── Resources ───────────────────────────────────────────────────────────────
1122
+ function findCollectionsDir() {
1123
+ const projectDir = getProjectDir();
1124
+ const candidates = [
1125
+ resolve(projectDir, "app", "config", "collections"),
1126
+ resolve(projectDir, "config", "collections"),
1127
+ resolve(projectDir, "collections")
1128
+ ];
1129
+ for (const dir of candidates) {
1130
+ if (existsSync(dir))
1131
+ return dir;
1132
+ }
1133
+ return null;
1134
+ }
1135
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
1136
+ const resources = [];
1137
+ // Collection files
1138
+ const collectionsDir = findCollectionsDir();
1139
+ if (collectionsDir) {
1140
+ const files = readdirSync(collectionsDir).filter((f) => f.endsWith(".ts") && f !== "index.ts");
1141
+ for (const file of files) {
1142
+ const name = file.replace(/\.ts$/, "");
1143
+ resources.push({
1144
+ uri: `rebase://collections/${name}`,
1145
+ name: `Collection: ${name}`,
1146
+ description: `TypeScript collection definition for "${name}"`,
1147
+ mimeType: "text/typescript"
1148
+ });
1149
+ }
1150
+ }
1151
+ // Generated schema
1152
+ const projectDir = getProjectDir();
1153
+ const schemaPath = resolve(projectDir, "app", "backend", "src", "schema.generated.ts");
1154
+ if (existsSync(schemaPath)) {
1155
+ resources.push({
1156
+ uri: "rebase://schema",
1157
+ name: "Generated Drizzle Schema",
1158
+ description: "Auto-generated Drizzle ORM schema from collection definitions",
1159
+ mimeType: "text/typescript"
1160
+ });
1161
+ }
1162
+ return { resources };
1163
+ });
1164
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1165
+ const { uri } = request.params;
1166
+ const projectDir = getProjectDir();
1167
+ if (uri === "rebase://schema") {
1168
+ const schemaPath = resolve(projectDir, "app", "backend", "src", "schema.generated.ts");
1169
+ if (!existsSync(schemaPath)) {
1170
+ throw new Error("Generated schema not found. Run `rebase schema generate` first.");
1171
+ }
1172
+ return {
1173
+ contents: [{
1174
+ uri,
1175
+ mimeType: "text/typescript",
1176
+ text: readFileSync(schemaPath, "utf-8")
1177
+ }]
1178
+ };
1179
+ }
1180
+ const collectionMatch = uri.match(/^rebase:\/\/collections\/(.+)$/);
1181
+ if (collectionMatch) {
1182
+ const name = collectionMatch[1];
1183
+ const collectionsDir = findCollectionsDir();
1184
+ if (!collectionsDir)
1185
+ throw new Error("Collections directory not found.");
1186
+ const absoluteCollectionsDir = resolve(collectionsDir);
1187
+ const filePath = resolve(absoluteCollectionsDir, `${name}.ts`);
1188
+ if (!filePath.startsWith(absoluteCollectionsDir)) {
1189
+ throw new Error("Access denied: path traversal detected");
1190
+ }
1191
+ if (!existsSync(filePath))
1192
+ throw new Error(`Collection "${name}" not found.`);
1193
+ return {
1194
+ contents: [{
1195
+ uri,
1196
+ mimeType: "text/typescript",
1197
+ text: readFileSync(filePath, "utf-8")
1198
+ }]
1199
+ };
1200
+ }
1201
+ throw new Error(`Unknown resource: ${uri}`);
1202
+ });
1203
+ // ── Start ───────────────────────────────────────────────────────────────────
1204
+ async function main() {
1205
+ const transport = new StdioServerTransport();
1206
+ await server.connect(transport);
1207
+ }
1208
+ if (process.env.NODE_ENV !== "test") {
1209
+ main().catch(console.error);
1210
+ }
1211
+ //# sourceMappingURL=index.js.map