@tupaas/mcp 1.2.2 → 1.3.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.
Files changed (2) hide show
  1. package/dist/index.js +160 -7
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,18 +6,32 @@ import fs from "fs";
6
6
  import path from "path";
7
7
  import os from "os";
8
8
  import { execSync } from "child_process";
9
- const VERSION = "1.2.2";
9
+ const VERSION = "1.3.0";
10
10
  const API_URL = process.env.TUPAAS_API_URL ?? "https://tupaas.dev/api";
11
11
  const API_KEY = process.env.TUPAAS_API_KEY ?? "";
12
- const server = new Server({ name: "tupaas-mcp", version: VERSION }, { capabilities: { tools: {} } });
12
+ const CONFIG_FILE = ".tupaas.json";
13
13
  function authHeaders() {
14
14
  return { "x-api-key": API_KEY };
15
15
  }
16
+ function readConfig(projectPath) {
17
+ const configPath = path.join(projectPath, CONFIG_FILE);
18
+ try {
19
+ return JSON.parse(fs.readFileSync(configPath, "utf-8"));
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ function writeConfig(projectPath, config) {
26
+ const configPath = path.join(projectPath, CONFIG_FILE);
27
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
28
+ }
29
+ const server = new Server({ name: "tupaas-mcp", version: VERSION }, { capabilities: { tools: {} } });
16
30
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
17
31
  tools: [
18
32
  {
19
33
  name: "deploy",
20
- description: "Deploy a project folder to TuPaaS. Compresses the folder, uploads it, and returns the live URL. Supports redeploy (same name = update existing project). IMPORTANT: If the project requires a database (e.g. has prisma, pg, DATABASE_URL in .env), call list_databases first and ask the user which database to use or whether to create a new one. Do not decide on your own.",
34
+ description: "Deploy a project folder to TuPaaS. Reads .tupaas.json from the project root to determine if this is a redeploy. After a successful deploy, writes/updates .tupaas.json with project info. IMPORTANT: If the project requires a database (e.g. has prisma, pg, DATABASE_URL in .env), call list_databases first and ask the user which database to use or whether to create a new one. Do not decide on your own. Before deploying, if no .tupaas.json exists, call list_projects to check if the project already exists to avoid creating duplicates.",
21
35
  inputSchema: {
22
36
  type: "object",
23
37
  properties: {
@@ -27,7 +41,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
27
41
  },
28
42
  app_name: {
29
43
  type: "string",
30
- description: "Name for the app (optional, inferred from directory name). Use the same name to redeploy/update an existing project.",
44
+ description: "Name for the app. If .tupaas.json exists in the project, this is ignored and the saved name is used. Only needed for first deploy.",
31
45
  },
32
46
  env_vars: {
33
47
  type: "object",
@@ -52,6 +66,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
52
66
  required: ["deploy_id"],
53
67
  },
54
68
  },
69
+ {
70
+ name: "list_projects",
71
+ description: "List all projects owned by the authenticated user. Returns slug, name, status, subdomain, and last deploy date. Use this before deploying to check if a project already exists — if it does, use the same app_name to redeploy instead of creating a new project.",
72
+ inputSchema: {
73
+ type: "object",
74
+ properties: {},
75
+ },
76
+ },
55
77
  {
56
78
  name: "list_databases",
57
79
  description: "List all databases owned by the authenticated user. Returns id, name, type, status, and connection URL for each database. IMPORTANT: If this is called as part of a deploy process and the project needs a database, you MUST present the list to the user and ask whether they want to use one of these existing databases or create a new one. Never choose automatically.",
@@ -88,6 +110,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
88
110
  required: ["database_id"],
89
111
  },
90
112
  },
113
+ {
114
+ name: "init",
115
+ description: "Create or update .tupaas.json config file in a project folder. Use this to link a local project to an existing TuPaaS project.",
116
+ inputSchema: {
117
+ type: "object",
118
+ properties: {
119
+ project_path: {
120
+ type: "string",
121
+ description: "Absolute path to the project folder",
122
+ },
123
+ project_slug: {
124
+ type: "string",
125
+ description: "Slug of the existing TuPaaS project to link to",
126
+ },
127
+ database_id: {
128
+ type: "string",
129
+ description: "Database ID to associate (optional)",
130
+ },
131
+ },
132
+ required: ["project_path", "project_slug"],
133
+ },
134
+ },
91
135
  {
92
136
  name: "version",
93
137
  description: "Returns the current version of the TuPaaS MCP server and the API URL it is configured to use.",
@@ -113,7 +157,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
113
157
  case "deploy": {
114
158
  const args = request.params.arguments;
115
159
  const projectPath = args.project_path;
116
- const appName = args.app_name ?? path.basename(projectPath);
117
160
  if (!fs.existsSync(projectPath)) {
118
161
  return {
119
162
  content: [
@@ -124,10 +167,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
124
167
  ],
125
168
  };
126
169
  }
170
+ // Read .tupaas.json config
171
+ const config = readConfig(projectPath);
172
+ let appName;
173
+ if (config?.projectSlug) {
174
+ // Verify project still exists
175
+ const projRes = await fetch(`${API_URL}/projects`, {
176
+ headers: authHeaders(),
177
+ });
178
+ const projects = projRes.ok
179
+ ? (await projRes.json())
180
+ : [];
181
+ const exists = projects.some((p) => p.slug === config.projectSlug);
182
+ if (exists) {
183
+ appName = config.appName ?? config.projectSlug;
184
+ }
185
+ else {
186
+ // Project was deleted — use provided name or directory name
187
+ appName = args.app_name ?? path.basename(projectPath);
188
+ }
189
+ }
190
+ else {
191
+ appName = args.app_name ?? path.basename(projectPath);
192
+ }
127
193
  // Compress as ZIP
128
194
  const zipPath = path.join(os.tmpdir(), `${appName}.zip`);
129
195
  try {
130
- execSync(`cd "${projectPath}" && zip -r "${zipPath}" . -x 'node_modules/*' '.git/*' '.next/*' 'dist/*'`);
196
+ execSync(`cd "${projectPath}" && zip -r "${zipPath}" . -x 'node_modules/*' '.git/*' '.next/*' 'dist/*' '.tupaas.json'`);
131
197
  }
132
198
  catch {
133
199
  return {
@@ -179,6 +245,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
179
245
  };
180
246
  }
181
247
  const result = (await res.json());
248
+ // Write .tupaas.json
249
+ const newConfig = {
250
+ ...config,
251
+ projectSlug: result.projectSlug,
252
+ appName,
253
+ lastDeployId: result.deployId,
254
+ };
255
+ writeConfig(projectPath, newConfig);
182
256
  // Poll until done
183
257
  let finalStatus = "QUEUED";
184
258
  let appUrl = result.appUrl;
@@ -198,12 +272,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
198
272
  if (status.done)
199
273
  break;
200
274
  }
275
+ // Check .gitignore suggestion
276
+ let gitignoreHint = "";
277
+ const gitignorePath = path.join(projectPath, ".gitignore");
278
+ try {
279
+ const gitignore = fs.existsSync(gitignorePath)
280
+ ? fs.readFileSync(gitignorePath, "utf-8")
281
+ : "";
282
+ if (!gitignore.includes(".tupaas.json")) {
283
+ gitignoreHint =
284
+ "\n\nTip: Add .tupaas.json to your .gitignore to avoid committing deploy config.";
285
+ }
286
+ }
287
+ catch {
288
+ /* ignore */
289
+ }
201
290
  if (finalStatus === "SUCCESS") {
202
291
  return {
203
292
  content: [
204
293
  {
205
294
  type: "text",
206
- text: `Deploy successful!\n\nProject: ${result.projectSlug}\nURL: ${appUrl}\nDeploy ID: ${result.deployId}`,
295
+ text: `Deploy successful!\n\nProject: ${result.projectSlug}\nURL: ${appUrl}\nDeploy ID: ${result.deployId}\nConfig saved to: ${CONFIG_FILE}${gitignoreHint}`,
207
296
  },
208
297
  ],
209
298
  };
@@ -239,6 +328,43 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
239
328
  ],
240
329
  };
241
330
  }
331
+ case "list_projects": {
332
+ const res = await fetch(`${API_URL}/projects`, {
333
+ headers: authHeaders(),
334
+ });
335
+ if (!res.ok) {
336
+ return {
337
+ content: [
338
+ {
339
+ type: "text",
340
+ text: `Error: Could not list projects (${res.status})`,
341
+ },
342
+ ],
343
+ };
344
+ }
345
+ const projects = (await res.json());
346
+ if (projects.length === 0) {
347
+ return {
348
+ content: [
349
+ {
350
+ type: "text",
351
+ text: "No projects found.",
352
+ },
353
+ ],
354
+ };
355
+ }
356
+ const list = projects
357
+ .map((p) => `- ${p.name} (${p.status})\n Slug: ${p.slug}\n URL: ${p.subdomain ? `https://${p.subdomain}` : "not deployed"}${p.lastDeployAt ? `\n Last deploy: ${p.lastDeployAt}` : ""}`)
358
+ .join("\n\n");
359
+ return {
360
+ content: [
361
+ {
362
+ type: "text",
363
+ text: `Projects:\n\n${list}`,
364
+ },
365
+ ],
366
+ };
367
+ }
242
368
  case "list_databases": {
243
369
  const res = await fetch(`${API_URL}/databases`, {
244
370
  headers: authHeaders(),
@@ -334,6 +460,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
334
460
  ],
335
461
  };
336
462
  }
463
+ case "init": {
464
+ const args = request.params.arguments;
465
+ if (!fs.existsSync(args.project_path)) {
466
+ return {
467
+ content: [
468
+ {
469
+ type: "text",
470
+ text: `Error: Path "${args.project_path}" does not exist`,
471
+ },
472
+ ],
473
+ };
474
+ }
475
+ const config = {
476
+ projectSlug: args.project_slug,
477
+ appName: args.project_slug,
478
+ ...(args.database_id ? { databaseId: args.database_id } : {}),
479
+ };
480
+ writeConfig(args.project_path, config);
481
+ return {
482
+ content: [
483
+ {
484
+ type: "text",
485
+ text: `Created ${CONFIG_FILE} in ${args.project_path}\n\n${JSON.stringify(config, null, 2)}\n\nThis project is now linked to TuPaaS project "${args.project_slug}". Future deploys will update this project.`,
486
+ },
487
+ ],
488
+ };
489
+ }
337
490
  case "version": {
338
491
  return {
339
492
  content: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tupaas/mcp",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "MCP server for deploying to TuPaaS from Claude Code",
5
5
  "type": "module",
6
6
  "bin": {