@tupaas/mcp 1.2.2 → 1.3.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 +161 -8
- 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.
|
|
9
|
+
const VERSION = "1.3.1";
|
|
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
|
|
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.
|
|
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
|
|
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
|
};
|
|
@@ -213,7 +302,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
213
302
|
content: [
|
|
214
303
|
{
|
|
215
304
|
type: "text",
|
|
216
|
-
text: `Deploy
|
|
305
|
+
text: `Deploy FAILED\n\nDeploy ID: ${result.deployId}\nProject: ${result.projectSlug}\nStatus: ${finalStatus}\n\n--- Logs ---\n${lastLogs || "No logs available"}\n--- End Logs ---\n\nAnalyze the logs above to identify the error. Common causes: missing environment variables, wrong start command, port mismatch, or missing dependencies. Fix the issue in the source code and redeploy.`,
|
|
217
306
|
},
|
|
218
307
|
],
|
|
219
308
|
};
|
|
@@ -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: [
|