@rebasepro/mcp-server 0.6.1 → 0.7.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/dist/index.js CHANGED
@@ -4,7 +4,8 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSche
4
4
  import { spawn } from "node:child_process";
5
5
  import { config as loadDotenv } from "dotenv";
6
6
  import { resolve } from "node:path";
7
- import { existsSync, readFileSync, readdirSync } from "node:fs";
7
+ import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from "node:fs";
8
+ import { homedir } from "node:os";
8
9
  /**
9
10
  * Detect the project's package manager by checking for lock files.
10
11
  * Falls back to pnpm (Rebase's default) if no lock file is found.
@@ -58,30 +59,190 @@ async function loadClientSdk() {
58
59
  const mod = await import(/* webpackIgnore: true */ CLIENT_PKG);
59
60
  return mod.createRebaseClient;
60
61
  }
61
- // ── Environment ─────────────────────────────────────────────────────────────
62
- const PROJECT_DIR = process.env.REBASE_PROJECT_DIR || process.cwd();
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();
63
174
  // Try to load .env from the project directory
64
175
  for (const envPath of [
65
- resolve(PROJECT_DIR, ".env"),
66
- resolve(PROJECT_DIR, "app", ".env")
176
+ resolve(ENV_PROJECT_DIR, ".env"),
177
+ resolve(ENV_PROJECT_DIR, "app", ".env")
67
178
  ]) {
68
179
  if (existsSync(envPath)) {
69
180
  loadDotenv({ path: envPath });
70
181
  break;
71
182
  }
72
183
  }
73
- const BASE_URL = process.env.REBASE_BASE_URL || "http://localhost:3001";
74
- const API_TOKEN = process.env.REBASE_API_TOKEN || process.env.REBASE_TOKEN || "";
75
- let _client = null;
76
- async function getClient() {
77
- if (!_client) {
78
- const createRebaseClient = await loadClientSdk();
79
- _client = createRebaseClient({
80
- baseUrl: BASE_URL,
81
- token: API_TOKEN || undefined
82
- });
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.`);
83
221
  }
84
- return _client;
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();
85
246
  }
86
247
  async function ensureAdmin() {
87
248
  const client = await getClient();
@@ -92,12 +253,13 @@ async function ensureAdmin() {
92
253
  }
93
254
  }
94
255
  catch (err) {
95
- throw new Error(`Admin authorization failed: ${err.message}`);
256
+ const msg = err instanceof Error ? err.message : String(err);
257
+ throw new Error(`Admin authorization failed: ${msg}`);
96
258
  }
97
259
  }
98
260
  // ── MCP Server ──────────────────────────────────────────────────────────────
99
261
  export const server = new Server({ name: "rebase-mcp-server",
100
- version: "0.0.1" }, { capabilities: { tools: {},
262
+ version: "0.1.0" }, { capabilities: { tools: {},
101
263
  resources: {} } });
102
264
  const CLI_TOOLS = [
103
265
  {
@@ -148,19 +310,6 @@ const CLI_TOOLS = [
148
310
  inputSchema: { type: "object", properties: {} },
149
311
  cmd: ["doctor"]
150
312
  },
151
- {
152
- name: "rebase_auth_reset_password",
153
- description: "Reset a user's password in the Rebase project.",
154
- inputSchema: {
155
- type: "object",
156
- properties: {
157
- email: { type: "string", description: "Email of the user to reset" },
158
- password: { type: "string", description: "New password to set (defaults to 'NewPassword123!')" }
159
- },
160
- required: ["email"]
161
- },
162
- cmd: ["auth", "reset-password"]
163
- },
164
313
  {
165
314
  name: "rebase_db_branch_create",
166
315
  description: "Create a new database branch (Admins only).",
@@ -223,7 +372,7 @@ const DATA_TOOLS = [
223
372
  where: {
224
373
  type: "object",
225
374
  description: "Filter object, e.g. { \"status\": \"eq.active\", \"price\": \"gte.100\" }",
226
- additionalProperties: { type: "string" }
375
+ additionalProperties: true
227
376
  }
228
377
  },
229
378
  required: ["collection"]
@@ -252,7 +401,7 @@ const DATA_TOOLS = [
252
401
  collection: { type: "string",
253
402
  description: "Collection slug" },
254
403
  data: { type: "object",
255
- description: "Document fields",
404
+ description: "Document data",
256
405
  additionalProperties: true }
257
406
  },
258
407
  required: ["collection", "data"]
@@ -349,6 +498,18 @@ const ADMIN_TOOLS = [
349
498
  description: "List all roles defined in the Rebase backend.",
350
499
  inputSchema: { type: "object",
351
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
+ }
352
513
  }
353
514
  ];
354
515
  const DEV_TOOLS = [
@@ -484,6 +645,59 @@ const FUNCTION_TOOLS = [
484
645
  }
485
646
  }
486
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
+ ];
487
701
  export const ALL_TOOLS = [
488
702
  ...CLI_TOOLS.map(({ cmd: _c, ...rest }) => rest),
489
703
  ...DATA_TOOLS,
@@ -491,7 +705,8 @@ export const ALL_TOOLS = [
491
705
  ...DEV_TOOLS,
492
706
  ...STORAGE_TOOLS,
493
707
  ...CRON_TOOLS,
494
- ...FUNCTION_TOOLS
708
+ ...FUNCTION_TOOLS,
709
+ ...PROJECT_TOOLS
495
710
  ];
496
711
  // ── Tool Handlers ───────────────────────────────────────────────────────────
497
712
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
@@ -499,13 +714,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
499
714
  }));
500
715
  /** Spawn the rebase CLI using the project's detected package manager. */
501
716
  function runRebaseCmd(commandArgs) {
502
- const pm = detectPackageManager(PROJECT_DIR);
717
+ const projectDir = getProjectDir();
718
+ const pm = detectPackageManager(projectDir);
503
719
  const { command, args: execArgs } = getExecCommand(pm);
504
720
  return new Promise((resolve) => {
505
721
  const child = spawn(command, [...execArgs, "rebase", ...commandArgs], {
506
- cwd: PROJECT_DIR,
722
+ cwd: projectDir,
507
723
  shell: true,
508
- env: { ...process.env }
724
+ env: {
725
+ ...process.env,
726
+ PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: "false"
727
+ }
509
728
  });
510
729
  const chunks = [];
511
730
  child.stdout?.on("data", (d) => chunks.push(d.toString()));
@@ -535,207 +754,377 @@ function jsonResult(data) {
535
754
  return textResult(JSON.stringify(data, null, 2));
536
755
  }
537
756
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
538
- const { name, arguments: args } = request.params;
539
- // ── CLI tools ───────────────────────────────────────────────────────
540
- const cliTool = CLI_TOOLS.find((t) => t.name === name);
541
- if (cliTool) {
542
- if (name.startsWith("rebase_db_branch_")) {
543
- await ensureAdmin();
544
- }
545
- let cmdArgs = [...cliTool.cmd];
546
- if (name === "rebase_auth_reset_password") {
547
- const argsObj = args;
548
- cmdArgs.push("--email", argsObj.email);
549
- if (argsObj.password) {
550
- cmdArgs.push("--password", argsObj.password);
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();
551
764
  }
552
- }
553
- else if (name === "rebase_db_branch_create") {
554
- const argsObj = args;
555
- cmdArgs.push(argsObj.name);
556
- if (argsObj.from) {
557
- cmdArgs.push("--from", argsObj.from);
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
+ }
558
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);
559
779
  }
560
- else if (name === "rebase_db_branch_delete" || name === "rebase_db_branch_info") {
561
- const argsObj = args;
562
- cmdArgs.push(argsObj.name);
563
- }
564
- const result = await runRebaseCmd(cmdArgs);
565
- return textResult(result);
566
- }
567
- // ── Data tools (via @rebasepro/client) ──────────────────────────────
568
- const client = await getClient();
569
- switch (name) {
570
- case "list_documents": {
571
- const argsObj = args;
572
- const { collection: slug, limit, offset, orderBy, where } = argsObj;
573
- const result = await client.data.collection(slug).find({
574
- limit,
575
- offset,
576
- orderBy,
577
- where
578
- });
579
- return jsonResult(result);
580
- }
581
- case "get_document": {
582
- const argsObj = args;
583
- const { collection: slug, id } = argsObj;
584
- const entity = await client.data.collection(slug).findById(id);
585
- if (!entity)
586
- return textResult(`Document ${id} not found in ${slug}`);
587
- return jsonResult(entity);
588
- }
589
- case "create_document": {
590
- const argsObj = args;
591
- const { collection: slug, data } = argsObj;
592
- const entity = await client.data.collection(slug).create(data);
593
- return jsonResult(entity);
594
- }
595
- case "update_document": {
596
- const argsObj = args;
597
- const { collection: slug, id, data } = argsObj;
598
- const entity = await client.data.collection(slug).update(id, data);
599
- return jsonResult(entity);
600
- }
601
- case "delete_document": {
602
- const argsObj = args;
603
- const { collection: slug, id } = argsObj;
604
- await client.data.collection(slug).delete(id);
605
- return textResult(`Deleted document ${id} from ${slug}`);
606
- }
607
- // ── Admin tools ────────────────────────────────────────────────────
608
- case "list_users": {
609
- const result = await client.admin.listUsers();
610
- return jsonResult(result);
611
- }
612
- case "create_user": {
613
- const argsObj = args;
614
- const { email, displayName, password, roles } = argsObj;
615
- const result = await client.admin.createUser({ email,
616
- displayName,
617
- password,
618
- roles });
619
- return jsonResult(result);
620
- }
621
- case "update_user": {
622
- const argsObj = args;
623
- const { userId, email, displayName, roles } = argsObj;
624
- const result = await client.admin.updateUser(userId, { email,
625
- displayName,
626
- roles });
627
- return jsonResult(result);
628
- }
629
- case "delete_user": {
630
- const argsObj = args;
631
- const { userId } = argsObj;
632
- const result = await client.admin.deleteUser(userId);
633
- return jsonResult(result);
634
- }
635
- case "list_roles": {
636
- const result = await client.admin.listRoles();
637
- return jsonResult(result);
638
- }
639
- // ── Storage Tools ──────────────────────────────────────────────────
640
- case "storage_list_objects": {
641
- const argsObj = args;
642
- const { prefix = "", bucket, maxResults, pageToken } = argsObj;
643
- const result = await client.storage.listObjects(prefix, { bucket, maxResults, pageToken });
644
- return jsonResult(result);
645
- }
646
- case "storage_delete_object": {
647
- const argsObj = args;
648
- const { key, bucket } = argsObj;
649
- await client.storage.deleteObject(key, bucket);
650
- return textResult(`Deleted object "${key}" successfully.`);
651
- }
652
- case "storage_get_metadata": {
653
- const argsObj = args;
654
- const { key, bucket } = argsObj;
655
- const result = await client.storage.getSignedUrl(key, bucket);
656
- return jsonResult(result);
657
- }
658
- // ── Cron Tools ─────────────────────────────────────────────────────
659
- case "cron_list_jobs": {
660
- const result = await client.cron.listJobs();
661
- return jsonResult(result);
662
- }
663
- case "cron_get_job": {
664
- const argsObj = args;
665
- const result = await client.cron.getJob(argsObj.jobId);
666
- return jsonResult(result);
667
- }
668
- case "cron_trigger_job": {
669
- const argsObj = args;
670
- const result = await client.cron.triggerJob(argsObj.jobId);
671
- return jsonResult(result);
672
- }
673
- case "cron_get_job_logs": {
674
- const argsObj = args;
675
- const result = await client.cron.getJobLogs(argsObj.jobId, { limit: argsObj.limit });
676
- return jsonResult(result);
677
- }
678
- case "cron_toggle_job": {
679
- const argsObj = args;
680
- const result = await client.cron.toggleJob(argsObj.jobId, argsObj.enabled);
681
- return jsonResult(result);
682
- }
683
- // ── Function Tools ─────────────────────────────────────────────────
684
- case "invoke_function": {
685
- const argsObj = args;
686
- const { name: funcName, payload, method, path: funcPath } = argsObj;
687
- const result = await client.functions.invoke(funcName, payload, { method, path: funcPath });
688
- return jsonResult(result);
689
- }
690
- // ── Dev server management ──────────────────────────────────────────
691
- case "rebase_dev_start": {
692
- if (devProcess && !devProcess.killed) {
693
- return textResult("Dev server is already running (PID " + devProcess.pid + ")");
694
- }
695
- devLogs.length = 0;
696
- const pm = detectPackageManager(PROJECT_DIR);
697
- const { command: runCmd, args: runArgs } = getRunCommand(pm);
698
- devProcess = spawn(runCmd, [...runArgs, "dev"], {
699
- cwd: resolve(PROJECT_DIR, "app"),
700
- shell: true,
701
- env: { ...process.env }
702
- });
703
- devProcess.stdout?.on("data", (d) => appendDevLog(d.toString()));
704
- devProcess.stderr?.on("data", (d) => appendDevLog(d.toString()));
705
- devProcess.on("close", (code) => {
706
- appendDevLog(`\n[dev server exited with code ${code}]`);
707
- devProcess = null;
708
- });
709
- // Wait a moment for initial output
710
- await new Promise((r) => setTimeout(r, 2000));
711
- return textResult(`Dev server started (PID ${devProcess?.pid})\n\n${devLogs.join("")}`);
712
- }
713
- case "rebase_dev_logs": {
714
- const argsObj = args;
715
- const lineCount = argsObj?.lines ?? 50;
716
- const recent = devLogs.slice(-lineCount);
717
- if (recent.length === 0) {
718
- return textResult(devProcess ? "No output captured yet." : "Dev server is not running.");
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
+ }
719
919
  }
720
- return textResult(recent.join(""));
721
920
  }
722
- case "rebase_dev_stop": {
723
- if (!devProcess || devProcess.killed) {
724
- return textResult("Dev server is not running.");
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);
725
1065
  }
726
- devProcess.kill("SIGTERM");
727
- return textResult("Dev server stopped.");
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}`);
728
1108
  }
729
- default:
730
- throw new Error(`Unknown tool: ${name}`);
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
+ };
731
1119
  }
732
1120
  });
733
1121
  // ── Resources ───────────────────────────────────────────────────────────────
734
1122
  function findCollectionsDir() {
1123
+ const projectDir = getProjectDir();
735
1124
  const candidates = [
736
- resolve(PROJECT_DIR, "app", "config", "collections"),
737
- resolve(PROJECT_DIR, "config", "collections"),
738
- resolve(PROJECT_DIR, "collections")
1125
+ resolve(projectDir, "app", "config", "collections"),
1126
+ resolve(projectDir, "config", "collections"),
1127
+ resolve(projectDir, "collections")
739
1128
  ];
740
1129
  for (const dir of candidates) {
741
1130
  if (existsSync(dir))
@@ -760,7 +1149,8 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
760
1149
  }
761
1150
  }
762
1151
  // Generated schema
763
- const schemaPath = resolve(PROJECT_DIR, "app", "backend", "src", "schema.generated.ts");
1152
+ const projectDir = getProjectDir();
1153
+ const schemaPath = resolve(projectDir, "app", "backend", "src", "schema.generated.ts");
764
1154
  if (existsSync(schemaPath)) {
765
1155
  resources.push({
766
1156
  uri: "rebase://schema",
@@ -773,8 +1163,9 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
773
1163
  });
774
1164
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
775
1165
  const { uri } = request.params;
1166
+ const projectDir = getProjectDir();
776
1167
  if (uri === "rebase://schema") {
777
- const schemaPath = resolve(PROJECT_DIR, "app", "backend", "src", "schema.generated.ts");
1168
+ const schemaPath = resolve(projectDir, "app", "backend", "src", "schema.generated.ts");
778
1169
  if (!existsSync(schemaPath)) {
779
1170
  throw new Error("Generated schema not found. Run `rebase schema generate` first.");
780
1171
  }