@tupaas/mcp 1.0.0 → 1.1.1

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 +164 -24
  2. package/package.json +4 -2
package/dist/index.js CHANGED
@@ -8,12 +8,15 @@ import os from "os";
8
8
  import { execSync } from "child_process";
9
9
  const API_URL = process.env.TUPAAS_API_URL ?? "https://tupaas.dev/api";
10
10
  const API_KEY = process.env.TUPAAS_API_KEY ?? "";
11
- const server = new Server({ name: "tupaas-mcp", version: "1.0.0" }, { capabilities: { tools: {} } });
11
+ const server = new Server({ name: "tupaas-mcp", version: "1.1.0" }, { capabilities: { tools: {} } });
12
+ function authHeaders() {
13
+ return { "x-api-key": API_KEY };
14
+ }
12
15
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
13
16
  tools: [
14
17
  {
15
18
  name: "deploy",
16
- description: "Deploy a project folder to TuPaaS. Compresses the folder, uploads it, and returns the live URL.",
19
+ 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.",
17
20
  inputSchema: {
18
21
  type: "object",
19
22
  properties: {
@@ -23,7 +26,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
23
26
  },
24
27
  app_name: {
25
28
  type: "string",
26
- description: "Name for the app (optional, inferred from directory name)",
29
+ description: "Name for the app (optional, inferred from directory name). Use the same name to redeploy/update an existing project.",
30
+ },
31
+ env_vars: {
32
+ type: "object",
33
+ description: "Environment variables to set on the app (e.g. {\"DATABASE_URL\": \"postgres://...\"}). These are saved and persist across deploys.",
34
+ additionalProperties: { type: "string" },
27
35
  },
28
36
  },
29
37
  required: ["project_path"],
@@ -43,6 +51,42 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
43
51
  required: ["deploy_id"],
44
52
  },
45
53
  },
54
+ {
55
+ name: "list_databases",
56
+ 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.",
57
+ inputSchema: {
58
+ type: "object",
59
+ properties: {},
60
+ },
61
+ },
62
+ {
63
+ name: "create_database",
64
+ description: "Create a new PostgreSQL database. Returns the database id, name, and internal connection URL. Use this URL as DATABASE_URL env var when deploying.",
65
+ inputSchema: {
66
+ type: "object",
67
+ properties: {
68
+ name: {
69
+ type: "string",
70
+ description: "Name for the database (lowercase, alphanumeric and hyphens only)",
71
+ },
72
+ },
73
+ required: ["name"],
74
+ },
75
+ },
76
+ {
77
+ name: "get_database",
78
+ description: "Get details of a specific database including its connection URL.",
79
+ inputSchema: {
80
+ type: "object",
81
+ properties: {
82
+ database_id: {
83
+ type: "string",
84
+ description: "The database ID",
85
+ },
86
+ },
87
+ required: ["database_id"],
88
+ },
89
+ },
46
90
  ],
47
91
  }));
48
92
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -51,7 +95,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
51
95
  content: [
52
96
  {
53
97
  type: "text",
54
- text: "Error: TUPAAS_API_KEY environment variable not set. Get your key from https://tupaas.dev/dashboard/api-keys",
98
+ text: "Error: TUPAAS_API_KEY environment variable not set. Get your key from your TuPaaS dashboard under API Keys.",
55
99
  },
56
100
  ],
57
101
  };
@@ -61,7 +105,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
61
105
  const args = request.params.arguments;
62
106
  const projectPath = args.project_path;
63
107
  const appName = args.app_name ?? path.basename(projectPath);
64
- // Validate path exists
65
108
  if (!fs.existsSync(projectPath)) {
66
109
  return {
67
110
  content: [
@@ -72,10 +115,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
72
115
  ],
73
116
  };
74
117
  }
75
- // Compress as ZIP (server expects ZIP format)
76
- const tarPath = path.join(os.tmpdir(), `${appName}.zip`);
118
+ // Compress as ZIP
119
+ const zipPath = path.join(os.tmpdir(), `${appName}.zip`);
77
120
  try {
78
- execSync(`cd "${projectPath}" && zip -r "${tarPath}" . -x 'node_modules/*' '.git/*' '.next/*' 'dist/*'`);
121
+ execSync(`cd "${projectPath}" && zip -r "${zipPath}" . -x 'node_modules/*' '.git/*' '.next/*' 'dist/*'`);
79
122
  }
80
123
  catch {
81
124
  return {
@@ -87,9 +130,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
87
130
  ],
88
131
  };
89
132
  }
90
- const stats = fs.statSync(tarPath);
133
+ const stats = fs.statSync(zipPath);
91
134
  if (stats.size > 50 * 1024 * 1024) {
92
- fs.unlinkSync(tarPath);
135
+ fs.unlinkSync(zipPath);
93
136
  return {
94
137
  content: [
95
138
  {
@@ -100,25 +143,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
100
143
  };
101
144
  }
102
145
  // Upload
103
- const fileBuffer = fs.readFileSync(tarPath);
146
+ const fileBuffer = fs.readFileSync(zipPath);
104
147
  const formData = new FormData();
105
148
  formData.append("file", new Blob([fileBuffer]), `${appName}.zip`);
106
149
  formData.append("name", appName);
150
+ if (args.env_vars && Object.keys(args.env_vars).length > 0) {
151
+ formData.append("env_vars", JSON.stringify(args.env_vars));
152
+ }
107
153
  const res = await fetch(`${API_URL}/deploy`, {
108
154
  method: "POST",
109
- headers: { "x-api-key": API_KEY },
155
+ headers: authHeaders(),
110
156
  body: formData,
111
157
  });
112
- fs.unlinkSync(tarPath);
158
+ fs.unlinkSync(zipPath);
113
159
  if (!res.ok) {
114
- const err = (await res.json());
160
+ let errorMsg = `Deploy failed (${res.status})`;
161
+ try {
162
+ const err = (await res.json());
163
+ errorMsg = `Deploy failed: ${err.error}`;
164
+ }
165
+ catch {
166
+ /* response not JSON */
167
+ }
115
168
  return {
116
- content: [
117
- {
118
- type: "text",
119
- text: `Deploy failed: ${err.error}`,
120
- },
121
- ],
169
+ content: [{ type: "text", text: errorMsg }],
122
170
  };
123
171
  }
124
172
  const result = (await res.json());
@@ -129,7 +177,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
129
177
  const maxPolls = 120;
130
178
  for (let i = 0; i < maxPolls; i++) {
131
179
  await new Promise((r) => setTimeout(r, 5000));
132
- const statusRes = await fetch(`${API_URL}/deploys/${result.deployId}/status`, { headers: { "x-api-key": API_KEY } });
180
+ const statusRes = await fetch(`${API_URL}/deploys/${result.deployId}/status`, { headers: authHeaders() });
133
181
  if (!statusRes.ok)
134
182
  break;
135
183
  const status = (await statusRes.json());
@@ -156,7 +204,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
156
204
  content: [
157
205
  {
158
206
  type: "text",
159
- text: `Deploy failed with status: ${finalStatus}\n\nDeploy ID: ${result.deployId}\nProject: ${result.projectSlug}\n\n--- Build Logs ---\n${lastLogs || "No logs available"}\n\n--- End Logs ---\nDashboard: http://localhost:3000/dashboard/projects/${result.projectSlug}`,
207
+ text: `Deploy ended with status: ${finalStatus}\n\nDeploy ID: ${result.deployId}\nProject: ${result.projectSlug}\n\n--- Build Logs ---\n${lastLogs || "No logs available"}\n\n--- End Logs ---`,
160
208
  },
161
209
  ],
162
210
  };
@@ -164,7 +212,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
164
212
  }
165
213
  case "deploy_status": {
166
214
  const args = request.params.arguments;
167
- const res = await fetch(`${API_URL}/deploys/${args.deploy_id}/status`, { headers: { "x-api-key": API_KEY } });
215
+ const res = await fetch(`${API_URL}/deploys/${args.deploy_id}/status`, { headers: authHeaders() });
168
216
  if (!res.ok) {
169
217
  return {
170
218
  content: [
@@ -176,11 +224,103 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
176
224
  };
177
225
  }
178
226
  const data = (await res.json());
227
+ return {
228
+ content: [
229
+ { type: "text", text: JSON.stringify(data, null, 2) },
230
+ ],
231
+ };
232
+ }
233
+ case "list_databases": {
234
+ const res = await fetch(`${API_URL}/databases`, {
235
+ headers: authHeaders(),
236
+ });
237
+ if (!res.ok) {
238
+ return {
239
+ content: [
240
+ {
241
+ type: "text",
242
+ text: `Error: Could not list databases (${res.status})`,
243
+ },
244
+ ],
245
+ };
246
+ }
247
+ const databases = (await res.json());
248
+ if (databases.length === 0) {
249
+ return {
250
+ content: [
251
+ {
252
+ type: "text",
253
+ text: "No databases found. Use create_database to create one.",
254
+ },
255
+ ],
256
+ };
257
+ }
258
+ const list = databases
259
+ .map((db) => `- ${db.name} (${db.type}, ${db.status})\n ID: ${db.id}\n URL: ${db.internalDbUrl ?? "hidden"}`)
260
+ .join("\n\n");
261
+ return {
262
+ content: [
263
+ {
264
+ type: "text",
265
+ text: `Databases:\n\n${list}`,
266
+ },
267
+ ],
268
+ };
269
+ }
270
+ case "create_database": {
271
+ const args = request.params.arguments;
272
+ const res = await fetch(`${API_URL}/databases`, {
273
+ method: "POST",
274
+ headers: {
275
+ ...authHeaders(),
276
+ "Content-Type": "application/json",
277
+ },
278
+ body: JSON.stringify({ name: args.name }),
279
+ });
280
+ if (!res.ok) {
281
+ let errorMsg = `Failed to create database (${res.status})`;
282
+ try {
283
+ const err = (await res.json());
284
+ errorMsg = `Failed to create database: ${err.error}`;
285
+ }
286
+ catch {
287
+ /* response not JSON */
288
+ }
289
+ return {
290
+ content: [{ type: "text", text: errorMsg }],
291
+ };
292
+ }
293
+ const database = (await res.json());
294
+ return {
295
+ content: [
296
+ {
297
+ type: "text",
298
+ text: `Database created!\n\nName: ${database.name}\nID: ${database.id}\nConnection URL: ${database.internalDbUrl}\n\nUse this URL as DATABASE_URL when deploying:\n deploy(env_vars: {"DATABASE_URL": "${database.internalDbUrl}"})`,
299
+ },
300
+ ],
301
+ };
302
+ }
303
+ case "get_database": {
304
+ const args = request.params.arguments;
305
+ const res = await fetch(`${API_URL}/databases/${args.database_id}`, {
306
+ headers: authHeaders(),
307
+ });
308
+ if (!res.ok) {
309
+ return {
310
+ content: [
311
+ {
312
+ type: "text",
313
+ text: `Error: Could not fetch database (${res.status})`,
314
+ },
315
+ ],
316
+ };
317
+ }
318
+ const database = (await res.json());
179
319
  return {
180
320
  content: [
181
321
  {
182
322
  type: "text",
183
- text: JSON.stringify(data, null, 2),
323
+ text: `Database: ${database.name}\nType: ${database.type}\nStatus: ${database.status}\nConnection URL: ${database.internalDbUrl}\n\nUse as DATABASE_URL in deploy env_vars.`,
184
324
  },
185
325
  ],
186
326
  };
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@tupaas/mcp",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "MCP server for deploying to TuPaaS from Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "tupaas-mcp": "./dist/index.js"
8
8
  },
9
- "files": ["dist/"],
9
+ "files": [
10
+ "dist/"
11
+ ],
10
12
  "engines": {
11
13
  "node": ">=18.0.0"
12
14
  },