@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.
- package/dist/index.js +164 -24
- 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.
|
|
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
|
|
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
|
|
76
|
-
const
|
|
118
|
+
// Compress as ZIP
|
|
119
|
+
const zipPath = path.join(os.tmpdir(), `${appName}.zip`);
|
|
77
120
|
try {
|
|
78
|
-
execSync(`cd "${projectPath}" && zip -r "${
|
|
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(
|
|
133
|
+
const stats = fs.statSync(zipPath);
|
|
91
134
|
if (stats.size > 50 * 1024 * 1024) {
|
|
92
|
-
fs.unlinkSync(
|
|
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(
|
|
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:
|
|
155
|
+
headers: authHeaders(),
|
|
110
156
|
body: formData,
|
|
111
157
|
});
|
|
112
|
-
fs.unlinkSync(
|
|
158
|
+
fs.unlinkSync(zipPath);
|
|
113
159
|
if (!res.ok) {
|
|
114
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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.
|
|
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": [
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/"
|
|
11
|
+
],
|
|
10
12
|
"engines": {
|
|
11
13
|
"node": ">=18.0.0"
|
|
12
14
|
},
|