mcp-app-studio 0.5.0 → 0.5.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/README.md +1 -1
- package/dist/cli/index.js +247 -65
- package/package.json +7 -7
package/README.md
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -3,22 +3,48 @@ import fs2 from "fs";
|
|
|
3
3
|
import { createWriteStream } from "fs";
|
|
4
4
|
import { Readable } from "stream";
|
|
5
5
|
import { pipeline } from "stream/promises";
|
|
6
|
-
import
|
|
6
|
+
import path3 from "path";
|
|
7
7
|
import os from "os";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
import * as p from "@clack/prompts";
|
|
10
10
|
import pc from "picocolors";
|
|
11
11
|
import { extract } from "tar";
|
|
12
12
|
|
|
13
|
+
// src/cli/template-utils.ts
|
|
14
|
+
import path from "path";
|
|
15
|
+
function getGithubArchiveTarballUrl(repo, ref) {
|
|
16
|
+
return `https://github.com/${repo}/archive/${ref}.tar.gz`;
|
|
17
|
+
}
|
|
18
|
+
function isTarLinkEntry(entry) {
|
|
19
|
+
if (!entry || typeof entry !== "object") return false;
|
|
20
|
+
if (!("type" in entry)) return false;
|
|
21
|
+
const t = entry.type;
|
|
22
|
+
return t === "SymbolicLink" || t === "Link";
|
|
23
|
+
}
|
|
24
|
+
function assertSafeTarEntryPath(rootDir, entryPath) {
|
|
25
|
+
if (path.isAbsolute(entryPath)) {
|
|
26
|
+
throw new Error(`Template tarball contains an absolute path: ${entryPath}`);
|
|
27
|
+
}
|
|
28
|
+
const resolved = path.resolve(rootDir, entryPath);
|
|
29
|
+
if (resolved !== rootDir && !resolved.startsWith(`${rootDir}${path.sep}`)) {
|
|
30
|
+
throw new Error(`Template tarball contains an unsafe path: ${entryPath}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function filterTemplateTarEntry(rootDir, entryPath, entry) {
|
|
34
|
+
if (isTarLinkEntry(entry)) return false;
|
|
35
|
+
assertSafeTarEntryPath(rootDir, entryPath);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
13
39
|
// src/cli/utils.ts
|
|
14
40
|
import fs from "fs";
|
|
15
|
-
import
|
|
41
|
+
import path2 from "path";
|
|
16
42
|
function isValidProjectPath(name) {
|
|
17
43
|
if (!name || name.trim() === "") {
|
|
18
44
|
return { valid: false, error: "Project name is required" };
|
|
19
45
|
}
|
|
20
46
|
const trimmed = name.trim();
|
|
21
|
-
if (
|
|
47
|
+
if (path2.isAbsolute(trimmed)) {
|
|
22
48
|
return {
|
|
23
49
|
valid: false,
|
|
24
50
|
error: "Absolute paths are not allowed. Use a relative path or project name."
|
|
@@ -31,9 +57,9 @@ function isValidProjectPath(name) {
|
|
|
31
57
|
};
|
|
32
58
|
}
|
|
33
59
|
const cwd = process.cwd();
|
|
34
|
-
const resolved =
|
|
35
|
-
const relative =
|
|
36
|
-
if (relative.startsWith("..") ||
|
|
60
|
+
const resolved = path2.resolve(cwd, trimmed);
|
|
61
|
+
const relative = path2.relative(cwd, resolved);
|
|
62
|
+
if (relative.startsWith("..") || path2.isAbsolute(relative)) {
|
|
37
63
|
return {
|
|
38
64
|
valid: false,
|
|
39
65
|
error: "Project must be created within current directory."
|
|
@@ -58,7 +84,7 @@ function emptyDir(dir) {
|
|
|
58
84
|
if (!fs.existsSync(dir)) return;
|
|
59
85
|
for (const file of fs.readdirSync(dir)) {
|
|
60
86
|
if (file === ".git") continue;
|
|
61
|
-
fs.rmSync(
|
|
87
|
+
fs.rmSync(path2.join(dir, file), { recursive: true, force: true });
|
|
62
88
|
}
|
|
63
89
|
}
|
|
64
90
|
var DEPENDENCY_OVERRIDES = {
|
|
@@ -66,8 +92,8 @@ var DEPENDENCY_OVERRIDES = {
|
|
|
66
92
|
"@assistant-ui/react-ai-sdk": "^1.3.3",
|
|
67
93
|
"@assistant-ui/react-markdown": "^0.12.1"
|
|
68
94
|
};
|
|
69
|
-
function updatePackageJson(dir, name, description,
|
|
70
|
-
const pkgPath =
|
|
95
|
+
function updatePackageJson(dir, name, description, opts) {
|
|
96
|
+
const pkgPath = path2.join(dir, "package.json");
|
|
71
97
|
if (!fs.existsSync(pkgPath)) return;
|
|
72
98
|
try {
|
|
73
99
|
const content = fs.readFileSync(pkgPath, "utf-8");
|
|
@@ -78,12 +104,10 @@ function updatePackageJson(dir, name, description, includeServer) {
|
|
|
78
104
|
if (description) {
|
|
79
105
|
pkg["description"] = description;
|
|
80
106
|
}
|
|
81
|
-
if (includeServer) {
|
|
82
|
-
const scripts = pkg["scripts"] ?? {};
|
|
83
|
-
scripts["postinstall"] = "cd server && npm install";
|
|
84
|
-
pkg["scripts"] = scripts;
|
|
85
|
-
}
|
|
86
107
|
const deps = pkg["dependencies"] ?? {};
|
|
108
|
+
if (opts?.mcpAppStudioVersion && opts.mcpAppStudioVersion !== "0.0.0") {
|
|
109
|
+
deps["mcp-app-studio"] = `^${opts.mcpAppStudioVersion}`;
|
|
110
|
+
}
|
|
87
111
|
for (const [dep, version] of Object.entries(DEPENDENCY_OVERRIDES)) {
|
|
88
112
|
if (deps[dep]) {
|
|
89
113
|
deps[dep] = version;
|
|
@@ -107,9 +131,11 @@ function detectPackageManager() {
|
|
|
107
131
|
}
|
|
108
132
|
|
|
109
133
|
// src/cli/index.ts
|
|
110
|
-
var __dirname =
|
|
111
|
-
var TEMPLATE_REPO = "assistant-ui/mcp-app-studio-starter";
|
|
112
|
-
var
|
|
134
|
+
var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
135
|
+
var TEMPLATE_REPO = process.env["MCP_APP_STUDIO_TEMPLATE_REPO"] ?? "assistant-ui/mcp-app-studio-starter";
|
|
136
|
+
var TEMPLATE_REF = process.env["MCP_APP_STUDIO_TEMPLATE_REF"] ?? process.env["MCP_APP_STUDIO_TEMPLATE_BRANCH"] ?? "main";
|
|
137
|
+
var DOCS_URL = "https://github.com/assistant-ui/assistant-ui/tree/main/packages/mcp-app-studio";
|
|
138
|
+
var EXAMPLES_URL = "https://github.com/assistant-ui/mcp-app-studio-starter";
|
|
113
139
|
var REQUIRED_NODE_VERSION = { major: 20, minor: 9, patch: 0 };
|
|
114
140
|
function parseNodeVersion(version) {
|
|
115
141
|
const [majorRaw, minorRaw, patchRaw] = version.split(".");
|
|
@@ -139,7 +165,7 @@ function ensureSupportedNodeVersion() {
|
|
|
139
165
|
}
|
|
140
166
|
function getVersion() {
|
|
141
167
|
try {
|
|
142
|
-
const pkgPath =
|
|
168
|
+
const pkgPath = path3.resolve(__dirname, "../package.json");
|
|
143
169
|
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
|
|
144
170
|
return pkg.version || "0.0.0";
|
|
145
171
|
} catch {
|
|
@@ -163,7 +189,7 @@ ${pc.bold("Options:")}
|
|
|
163
189
|
--version, -v Show version number
|
|
164
190
|
-y, --yes Non-interactive mode (use defaults or flag values)
|
|
165
191
|
--template <name> Template to use: minimal, poi-map (default: minimal)
|
|
166
|
-
--include-server Include MCP server (default
|
|
192
|
+
--include-server Include MCP server (default: true)
|
|
167
193
|
--no-server Do not include MCP server
|
|
168
194
|
--description <text> App description
|
|
169
195
|
|
|
@@ -173,8 +199,8 @@ ${pc.bold("Examples:")}
|
|
|
173
199
|
npx mcp-app-studio my-app -y --template poi-map --include-server
|
|
174
200
|
|
|
175
201
|
${pc.bold("Learn more:")}
|
|
176
|
-
Documentation:
|
|
177
|
-
Examples:
|
|
202
|
+
Documentation: ${DOCS_URL}
|
|
203
|
+
Examples: ${EXAMPLES_URL}
|
|
178
204
|
`);
|
|
179
205
|
}
|
|
180
206
|
function quotePath(p2) {
|
|
@@ -201,6 +227,76 @@ var TEMPLATE_DEFAULT_COMPONENT = {
|
|
|
201
227
|
minimal: "welcome",
|
|
202
228
|
"poi-map": "poi-map"
|
|
203
229
|
};
|
|
230
|
+
function writeStudioConfig(targetDir, template, appName) {
|
|
231
|
+
const exportConfig = TEMPLATE_EXPORT_CONFIG[template];
|
|
232
|
+
const config = {
|
|
233
|
+
widget: {
|
|
234
|
+
entryPoint: exportConfig.entryPoint,
|
|
235
|
+
exportName: exportConfig.exportName,
|
|
236
|
+
name: appName
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
fs2.writeFileSync(
|
|
240
|
+
path3.join(targetDir, "mcp-app-studio.config.json"),
|
|
241
|
+
`${JSON.stringify(config, null, 2)}
|
|
242
|
+
`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
function updateServerPackageName(targetDir, projectPackageName) {
|
|
246
|
+
const serverPkgPath = path3.join(targetDir, "server", "package.json");
|
|
247
|
+
if (!fs2.existsSync(serverPkgPath)) return;
|
|
248
|
+
const raw = fs2.readFileSync(serverPkgPath, "utf-8");
|
|
249
|
+
const pkg = JSON.parse(raw);
|
|
250
|
+
const baseName = projectPackageName.includes("/") ? projectPackageName.split("/").pop() : projectPackageName;
|
|
251
|
+
if (!baseName) return;
|
|
252
|
+
pkg["name"] = `${baseName}-mcp-server`;
|
|
253
|
+
fs2.writeFileSync(serverPkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
254
|
+
`, "utf-8");
|
|
255
|
+
}
|
|
256
|
+
function ensureServerPostinstall(targetDir) {
|
|
257
|
+
const pkgPath = path3.join(targetDir, "package.json");
|
|
258
|
+
const serverPkgPath = path3.join(targetDir, "server", "package.json");
|
|
259
|
+
if (!fs2.existsSync(pkgPath) || !fs2.existsSync(serverPkgPath)) return;
|
|
260
|
+
const raw = fs2.readFileSync(pkgPath, "utf-8");
|
|
261
|
+
const pkg = JSON.parse(raw);
|
|
262
|
+
const scripts = pkg["scripts"] ?? {};
|
|
263
|
+
if (scripts["postinstall"]) return;
|
|
264
|
+
const scriptPath = path3.join(
|
|
265
|
+
targetDir,
|
|
266
|
+
"scripts",
|
|
267
|
+
"mcp-app-studio-postinstall.cjs"
|
|
268
|
+
);
|
|
269
|
+
fs2.mkdirSync(path3.dirname(scriptPath), { recursive: true });
|
|
270
|
+
fs2.writeFileSync(
|
|
271
|
+
scriptPath,
|
|
272
|
+
`/* eslint-disable */
|
|
273
|
+
const { spawnSync } = require("node:child_process");
|
|
274
|
+
const { existsSync } = require("node:fs");
|
|
275
|
+
const { join } = require("node:path");
|
|
276
|
+
|
|
277
|
+
const ROOT = process.cwd();
|
|
278
|
+
const SERVER_DIR = join(ROOT, "server");
|
|
279
|
+
|
|
280
|
+
if (!existsSync(join(SERVER_DIR, "package.json"))) process.exit(0);
|
|
281
|
+
if (existsSync(join(SERVER_DIR, "node_modules")) || existsSync(join(SERVER_DIR, ".pnp.cjs"))) process.exit(0);
|
|
282
|
+
|
|
283
|
+
const ua = process.env.npm_config_user_agent || "";
|
|
284
|
+
let pm = "npm";
|
|
285
|
+
if (ua.includes("pnpm")) pm = "pnpm";
|
|
286
|
+
else if (ua.includes("yarn")) pm = "yarn";
|
|
287
|
+
else if (ua.includes("bun")) pm = "bun";
|
|
288
|
+
|
|
289
|
+
console.log("\\n\\x1b[2mInstalling server dependencies (" + pm + ")...\\x1b[0m\\n");
|
|
290
|
+
const result = spawnSync(pm, ["install"], { cwd: SERVER_DIR, stdio: "inherit", shell: process.platform === "win32" });
|
|
291
|
+
process.exit(result.status == null ? 1 : result.status);
|
|
292
|
+
`,
|
|
293
|
+
"utf-8"
|
|
294
|
+
);
|
|
295
|
+
scripts["postinstall"] = "node scripts/mcp-app-studio-postinstall.cjs";
|
|
296
|
+
pkg["scripts"] = scripts;
|
|
297
|
+
fs2.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
298
|
+
`, "utf-8");
|
|
299
|
+
}
|
|
204
300
|
function generateComponentRegistry(components) {
|
|
205
301
|
const imports = [];
|
|
206
302
|
const entries = [];
|
|
@@ -326,8 +422,10 @@ function generateExamplesIndex(components) {
|
|
|
326
422
|
` : "// No examples\n";
|
|
327
423
|
}
|
|
328
424
|
function updateExportScriptDefaults(targetDir, entryPoint, exportName) {
|
|
329
|
-
const exportScriptPath =
|
|
425
|
+
const exportScriptPath = path3.join(targetDir, "scripts/export.ts");
|
|
426
|
+
if (!fs2.existsSync(exportScriptPath)) return;
|
|
330
427
|
let content = fs2.readFileSync(exportScriptPath, "utf-8");
|
|
428
|
+
if (content.includes("mcp-app-studio.config.json")) return;
|
|
331
429
|
content = content.replace(
|
|
332
430
|
/entryPoint: "lib\/workbench\/wrappers\/[^"]+"/,
|
|
333
431
|
`entryPoint: "${entryPoint}"`
|
|
@@ -358,19 +456,22 @@ function generateWorkbenchIndexExport(components) {
|
|
|
358
456
|
return exports.length > 0 ? `export { ${exports.join(", ")} } from "./wrappers";` : "// No SDK exports";
|
|
359
457
|
}
|
|
360
458
|
function updateWorkbenchIndex(targetDir, components) {
|
|
361
|
-
const indexPath =
|
|
459
|
+
const indexPath = path3.join(targetDir, "lib/workbench/index.ts");
|
|
362
460
|
let content = fs2.readFileSync(indexPath, "utf-8");
|
|
363
461
|
const wrappersExportRegex = /export \{[^}]*\} from "\.\/wrappers";/;
|
|
364
462
|
const newExport = generateWorkbenchIndexExport(components);
|
|
365
463
|
if (wrappersExportRegex.test(content)) {
|
|
366
464
|
content = content.replace(wrappersExportRegex, newExport);
|
|
367
465
|
} else {
|
|
368
|
-
content = content.trimEnd()
|
|
466
|
+
content = `${content.trimEnd()}
|
|
467
|
+
|
|
468
|
+
${newExport}
|
|
469
|
+
`;
|
|
369
470
|
}
|
|
370
471
|
fs2.writeFileSync(indexPath, content);
|
|
371
472
|
}
|
|
372
473
|
function updateWorkbenchStoreDefault(targetDir, defaultComponent) {
|
|
373
|
-
const storePath =
|
|
474
|
+
const storePath = path3.join(targetDir, "lib/workbench/store.ts");
|
|
374
475
|
let content = fs2.readFileSync(storePath, "utf-8");
|
|
375
476
|
content = content.replace(
|
|
376
477
|
/selectedComponent: "[^"]+"/,
|
|
@@ -380,17 +481,17 @@ function updateWorkbenchStoreDefault(targetDir, defaultComponent) {
|
|
|
380
481
|
}
|
|
381
482
|
function applyTemplate(targetDir, template) {
|
|
382
483
|
const components = TEMPLATE_COMPONENTS[template];
|
|
383
|
-
const registryPath =
|
|
484
|
+
const registryPath = path3.join(
|
|
384
485
|
targetDir,
|
|
385
486
|
"lib/workbench/component-registry.tsx"
|
|
386
487
|
);
|
|
387
488
|
fs2.writeFileSync(registryPath, generateComponentRegistry(components));
|
|
388
|
-
const wrappersIndexPath =
|
|
489
|
+
const wrappersIndexPath = path3.join(
|
|
389
490
|
targetDir,
|
|
390
491
|
"lib/workbench/wrappers/index.ts"
|
|
391
492
|
);
|
|
392
493
|
fs2.writeFileSync(wrappersIndexPath, generateWrappersIndex(components));
|
|
393
|
-
const examplesIndexPath =
|
|
494
|
+
const examplesIndexPath = path3.join(
|
|
394
495
|
targetDir,
|
|
395
496
|
"components/examples/index.ts"
|
|
396
497
|
);
|
|
@@ -398,23 +499,23 @@ function applyTemplate(targetDir, template) {
|
|
|
398
499
|
updateWorkbenchIndex(targetDir, components);
|
|
399
500
|
const defaultComponent = TEMPLATE_DEFAULT_COMPONENT[template];
|
|
400
501
|
updateWorkbenchStoreDefault(targetDir, defaultComponent);
|
|
401
|
-
const examplesDir =
|
|
502
|
+
const examplesDir = path3.join(targetDir, "components/examples");
|
|
402
503
|
if (!components.includes("welcome")) {
|
|
403
|
-
fs2.rmSync(
|
|
504
|
+
fs2.rmSync(path3.join(examplesDir, "welcome-card"), {
|
|
404
505
|
recursive: true,
|
|
405
506
|
force: true
|
|
406
507
|
});
|
|
407
508
|
fs2.rmSync(
|
|
408
|
-
|
|
509
|
+
path3.join(targetDir, "lib/workbench/wrappers/welcome-card-sdk.tsx"),
|
|
409
510
|
{ force: true }
|
|
410
511
|
);
|
|
411
512
|
}
|
|
412
513
|
if (!components.includes("poi-map")) {
|
|
413
|
-
fs2.rmSync(
|
|
514
|
+
fs2.rmSync(path3.join(examplesDir, "poi-map"), {
|
|
414
515
|
recursive: true,
|
|
415
516
|
force: true
|
|
416
517
|
});
|
|
417
|
-
fs2.rmSync(
|
|
518
|
+
fs2.rmSync(path3.join(targetDir, "lib/workbench/wrappers/poi-map-sdk.tsx"), {
|
|
418
519
|
force: true
|
|
419
520
|
});
|
|
420
521
|
}
|
|
@@ -425,16 +526,61 @@ function applyTemplate(targetDir, template) {
|
|
|
425
526
|
exportConfig.exportName
|
|
426
527
|
);
|
|
427
528
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
529
|
+
var REQUIRED_TEMPLATE_PATHS = [
|
|
530
|
+
"package.json",
|
|
531
|
+
"scripts/dev.ts",
|
|
532
|
+
"scripts/export.ts",
|
|
533
|
+
"lib/workbench/component-registry.tsx",
|
|
534
|
+
"lib/workbench/wrappers/index.ts",
|
|
535
|
+
"components/examples/index.ts",
|
|
536
|
+
"lib/workbench/index.ts",
|
|
537
|
+
"lib/workbench/store.ts"
|
|
538
|
+
];
|
|
539
|
+
function validateTemplateDir(templateDir) {
|
|
540
|
+
const missing = REQUIRED_TEMPLATE_PATHS.filter(
|
|
541
|
+
(p2) => !fs2.existsSync(path3.join(templateDir, p2))
|
|
542
|
+
);
|
|
543
|
+
if (missing.length > 0) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`Template validation failed. Missing expected files:
|
|
546
|
+
${missing.map((p2) => `- ${p2}`).join(
|
|
547
|
+
"\n"
|
|
548
|
+
)}
|
|
549
|
+
|
|
550
|
+
This may indicate the starter template has changed. Try updating mcp-app-studio or set MCP_APP_STUDIO_TEMPLATE_REPO / MCP_APP_STUDIO_TEMPLATE_REF to a compatible template.`
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function copyDirContents(srcDir, destDir) {
|
|
555
|
+
fs2.mkdirSync(destDir, { recursive: true });
|
|
556
|
+
const entries = fs2.readdirSync(srcDir, { withFileTypes: true });
|
|
557
|
+
for (const entry of entries) {
|
|
558
|
+
if (entry.name === ".git") continue;
|
|
559
|
+
const srcPath = path3.join(srcDir, entry.name);
|
|
560
|
+
const destPath = path3.join(destDir, entry.name);
|
|
561
|
+
if (entry.isDirectory()) {
|
|
562
|
+
copyDirContents(srcPath, destPath);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (entry.isFile()) {
|
|
566
|
+
fs2.mkdirSync(path3.dirname(destPath), { recursive: true });
|
|
567
|
+
fs2.copyFileSync(srcPath, destPath);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
async function downloadTemplateToTemp() {
|
|
572
|
+
const tarballUrl = getGithubArchiveTarballUrl(TEMPLATE_REPO, TEMPLATE_REF);
|
|
573
|
+
const tempDir = fs2.mkdtempSync(
|
|
574
|
+
path3.join(os.tmpdir(), "mcp-app-studio-template-")
|
|
575
|
+
);
|
|
576
|
+
const tarballPath = path3.join(tempDir, "template.tar.gz");
|
|
577
|
+
const extractDir = path3.join(tempDir, "extract");
|
|
432
578
|
try {
|
|
433
|
-
fs2.mkdirSync(
|
|
579
|
+
fs2.mkdirSync(extractDir, { recursive: true });
|
|
434
580
|
const response = await fetch(tarballUrl);
|
|
435
581
|
if (!response.ok) {
|
|
436
582
|
throw new Error(
|
|
437
|
-
`Failed to download template: ${response.status} ${response.statusText}`
|
|
583
|
+
`Failed to download template from ${tarballUrl}: ${response.status} ${response.statusText}`
|
|
438
584
|
);
|
|
439
585
|
}
|
|
440
586
|
const fileStream = createWriteStream(tarballPath);
|
|
@@ -446,15 +592,33 @@ async function downloadTemplate(targetDir) {
|
|
|
446
592
|
body
|
|
447
593
|
);
|
|
448
594
|
await pipeline(nodeStream, fileStream);
|
|
449
|
-
fs2.mkdirSync(targetDir, { recursive: true });
|
|
450
595
|
await extract({
|
|
451
596
|
file: tarballPath,
|
|
452
|
-
cwd:
|
|
453
|
-
strip:
|
|
454
|
-
|
|
597
|
+
cwd: extractDir,
|
|
598
|
+
strip: 0,
|
|
599
|
+
filter: (entryPath, entry) => filterTemplateTarEntry(extractDir, entryPath, entry)
|
|
455
600
|
});
|
|
456
|
-
|
|
457
|
-
|
|
601
|
+
const topLevelDirs = fs2.readdirSync(extractDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
602
|
+
if (topLevelDirs.length !== 1) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`Unexpected template archive layout. Expected a single top-level directory, found ${topLevelDirs.length}.`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const topLevelDir = topLevelDirs[0];
|
|
608
|
+
if (!topLevelDir) {
|
|
609
|
+
throw new Error(
|
|
610
|
+
"Unexpected template archive layout. No top-level directory found."
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
const templateDir = path3.join(extractDir, topLevelDir.name);
|
|
614
|
+
validateTemplateDir(templateDir);
|
|
615
|
+
return { tempDir, templateDir };
|
|
616
|
+
} catch (err) {
|
|
617
|
+
try {
|
|
618
|
+
fs2.rmSync(tempDir, { recursive: true, force: true });
|
|
619
|
+
} catch {
|
|
620
|
+
}
|
|
621
|
+
throw err;
|
|
458
622
|
}
|
|
459
623
|
}
|
|
460
624
|
function parseArgs(args) {
|
|
@@ -599,8 +763,8 @@ async function main() {
|
|
|
599
763
|
p.cancel("Operation cancelled.");
|
|
600
764
|
process.exit(0);
|
|
601
765
|
}
|
|
602
|
-
const targetDir =
|
|
603
|
-
const packageName = projectName === "." ? toValidPackageName(
|
|
766
|
+
const targetDir = path3.resolve(process.cwd(), projectName);
|
|
767
|
+
const packageName = projectName === "." ? toValidPackageName(path3.basename(targetDir)) : toValidPackageName(projectName);
|
|
604
768
|
const config = {
|
|
605
769
|
name: projectName,
|
|
606
770
|
packageName,
|
|
@@ -608,9 +772,11 @@ async function main() {
|
|
|
608
772
|
template,
|
|
609
773
|
includeServer
|
|
610
774
|
};
|
|
611
|
-
|
|
775
|
+
const targetNotEmpty = !isEmpty(targetDir);
|
|
776
|
+
let shouldOverwrite = false;
|
|
777
|
+
if (targetNotEmpty) {
|
|
612
778
|
if (nonInteractive) {
|
|
613
|
-
|
|
779
|
+
shouldOverwrite = true;
|
|
614
780
|
} else {
|
|
615
781
|
const overwrite = await p.confirm({
|
|
616
782
|
message: `Directory "${projectName}" is not empty. Remove existing files?`,
|
|
@@ -620,13 +786,14 @@ async function main() {
|
|
|
620
786
|
p.cancel("Operation cancelled.");
|
|
621
787
|
process.exit(0);
|
|
622
788
|
}
|
|
623
|
-
|
|
789
|
+
shouldOverwrite = true;
|
|
624
790
|
}
|
|
625
791
|
}
|
|
626
792
|
const s = p.spinner();
|
|
627
793
|
s.start("Downloading template...");
|
|
794
|
+
let downloaded;
|
|
628
795
|
try {
|
|
629
|
-
await
|
|
796
|
+
downloaded = await downloadTemplateToTemp();
|
|
630
797
|
} catch (error) {
|
|
631
798
|
s.stop("Download failed");
|
|
632
799
|
p.log.error(
|
|
@@ -635,16 +802,35 @@ async function main() {
|
|
|
635
802
|
process.exit(1);
|
|
636
803
|
}
|
|
637
804
|
s.message("Creating project...");
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
805
|
+
try {
|
|
806
|
+
if (!downloaded) {
|
|
807
|
+
throw new Error(
|
|
808
|
+
"Internal error: template download did not return a result"
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
if (shouldOverwrite) {
|
|
812
|
+
emptyDir(targetDir);
|
|
813
|
+
}
|
|
814
|
+
fs2.mkdirSync(targetDir, { recursive: true });
|
|
815
|
+
copyDirContents(downloaded.templateDir, targetDir);
|
|
816
|
+
} finally {
|
|
817
|
+
if (downloaded?.tempDir) {
|
|
818
|
+
fs2.rmSync(downloaded.tempDir, { recursive: true, force: true });
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
updatePackageJson(targetDir, config.packageName, config.description, {
|
|
822
|
+
mcpAppStudioVersion: getVersion()
|
|
823
|
+
});
|
|
824
|
+
if (config.includeServer) {
|
|
825
|
+
ensureServerPostinstall(targetDir);
|
|
826
|
+
}
|
|
644
827
|
s.message("Applying template...");
|
|
645
828
|
applyTemplate(targetDir, config.template);
|
|
646
|
-
|
|
647
|
-
|
|
829
|
+
writeStudioConfig(targetDir, config.template, path3.basename(targetDir));
|
|
830
|
+
if (config.includeServer) {
|
|
831
|
+
updateServerPackageName(targetDir, config.packageName);
|
|
832
|
+
} else {
|
|
833
|
+
fs2.rmSync(path3.join(targetDir, "server"), { recursive: true, force: true });
|
|
648
834
|
}
|
|
649
835
|
s.stop("Project created!");
|
|
650
836
|
const pm = detectPackageManager();
|
|
@@ -694,12 +880,8 @@ async function main() {
|
|
|
694
880
|
);
|
|
695
881
|
p.log.message("");
|
|
696
882
|
p.log.step(pc.bold("Learn more:"));
|
|
697
|
-
p.log.message(
|
|
698
|
-
|
|
699
|
-
);
|
|
700
|
-
p.log.message(
|
|
701
|
-
` ${pc.dim("\u2022")} Examples: ${pc.cyan("https://github.com/assistant-ui/mcp-app-studio-starter")}`
|
|
702
|
-
);
|
|
883
|
+
p.log.message(` ${pc.dim("\u2022")} SDK Docs: ${pc.cyan(DOCS_URL)}`);
|
|
884
|
+
p.log.message(` ${pc.dim("\u2022")} Examples: ${pc.cyan(EXAMPLES_URL)}`);
|
|
703
885
|
if (config.includeServer) {
|
|
704
886
|
p.log.message(
|
|
705
887
|
` ${pc.dim("\u2022")} MCP Guide: ${pc.cyan("https://modelcontextprotocol.io/quickstart")}`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-app-studio",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Build interactive apps for AI assistants (ChatGPT, Claude, MCP hosts)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chatgpt",
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
],
|
|
63
63
|
"sideEffects": false,
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@clack/prompts": "^0.
|
|
65
|
+
"@clack/prompts": "^1.0.0",
|
|
66
66
|
"picocolors": "^1.1.1",
|
|
67
|
-
"tar": "^7.5.
|
|
67
|
+
"tar": "^7.5.7"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"@modelcontextprotocol/ext-apps": ">=0.4.0",
|
|
@@ -79,12 +79,12 @@
|
|
|
79
79
|
}
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@modelcontextprotocol/ext-apps": "^0.
|
|
83
|
-
"@types/node": "^25.0
|
|
84
|
-
"@types/react": "^19.
|
|
82
|
+
"@modelcontextprotocol/ext-apps": "^1.0.1",
|
|
83
|
+
"@types/node": "^25.2.0",
|
|
84
|
+
"@types/react": "^19.2.10",
|
|
85
85
|
"@types/tar": "^6.1.13",
|
|
86
86
|
"@vitest/coverage-v8": "^4.0.18",
|
|
87
|
-
"react": "^19.
|
|
87
|
+
"react": "^19.2.4",
|
|
88
88
|
"tsup": "^8.5.1",
|
|
89
89
|
"tsx": "^4.21.0",
|
|
90
90
|
"typescript": "^5.9.3",
|