Skip to content

Conversation

@liady
Copy link
Collaborator

@liady liady commented Jan 25, 2026

Closes #354

Summary

Enable scaffolding new MCP App projects with a single command:

npm create @modelcontextprotocol/mcp-app my-app

Features

  • Interactive CLI with beautiful UX using @clack/prompts
  • Two frameworks: React and Vanilla JS
  • Uses tsx for running the dev server (broader compatibility than bun)
  • Uses esbuild for bundling server files
  • Supports command-line flags:
    • --framework react|vanillajs - Skip framework selection prompt
  • Always runs npm install after scaffolding

Package Structure

packages/create-mcp-app/
├── src/           # CLI source code
├── templates/
│   ├── base/      # Shared files (server.ts, main.ts, configs)
│   ├── react/     # React-specific files
│   └── vanillajs/ # Vanilla JS-specific files
├── test/          # E2E scaffold tests
└── dist/          # Compiled CLI

Changes

  • Add packages/create-mcp-app/ with CLI implementation and templates
  • Add packages/* to workspaces in root package.json
  • Add publish-packages job to npm-publish workflow
  • Update README to reference the new CLI tool
  • SDK version read dynamically from package.json at runtime
  • E2E test scaffolds both templates and verifies they build

Testing

# Build the package
npm run build --workspace packages/create-mcp-app

# Run scaffold E2E test (scaffolds + builds both templates)
npm test --workspace packages/create-mcp-app

# Manual test
cd /tmp
node /path/to/ext-apps/packages/create-mcp-app/dist/index.js my-app --framework react
cd my-app && npm run build

Test plan

  • Package builds successfully
  • React template scaffolds and builds
  • Vanilla JS template scaffolds and builds
  • CLI help displays correctly
  • E2E scaffold test passes (both templates)
  • Test with basic-host after merge

🤖 Generated with Claude Code

Enable scaffolding new MCP App projects via `npm create @modelcontextprotocol/mcp-app`.

Features:
- Interactive CLI with @clack/prompts for beautiful UX
- React and Vanilla JS templates included
- Uses tsx for broader compatibility (no bun dependency)
- Templates include server, UI, and build configuration
- Supports --template and --no-install flags

Changes:
- Add packages/create-mcp-app/ with CLI and templates
- Add packages/* to workspaces in root package.json
- Add publish-packages job to npm-publish workflow

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 25, 2026

Open in StackBlitz

@modelcontextprotocol/ext-apps

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/ext-apps@353

@modelcontextprotocol/server-arcade

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-arcade@353

@modelcontextprotocol/server-basic-react

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-basic-react@353

@modelcontextprotocol/server-basic-vanillajs

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-basic-vanillajs@353

@modelcontextprotocol/server-budget-allocator

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-budget-allocator@353

@modelcontextprotocol/server-cohort-heatmap

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-cohort-heatmap@353

@modelcontextprotocol/server-customer-segmentation

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-customer-segmentation@353

@modelcontextprotocol/server-map

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-map@353

@modelcontextprotocol/server-pdf

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-pdf@353

@modelcontextprotocol/server-scenario-modeler

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-scenario-modeler@353

@modelcontextprotocol/server-shadertoy

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-shadertoy@353

@modelcontextprotocol/server-sheet-music

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-sheet-music@353

@modelcontextprotocol/server-system-monitor

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-system-monitor@353

@modelcontextprotocol/server-threejs

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-threejs@353

@modelcontextprotocol/server-transcript

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-transcript@353

@modelcontextprotocol/server-video-resource

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-video-resource@353

@modelcontextprotocol/server-wiki-explorer

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-wiki-explorer@353

commit: 1df6b8d

liady and others added 2 commits January 25, 2026 22:50
- Add Quick Start section to README with npm create command
- Add tip callout to quickstart guide for faster project setup

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@liady liady marked this pull request as ready for review January 25, 2026 21:31
@liady liady requested a review from ochafik January 25, 2026 21:31
Copy link
Member

@jonathanhefner jonathanhefner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left some comments, but I think my biggest qualm is testing. I think we need to be 100% sure that the code we generate works end-to-end. Right now, it looks like the scaffold code isn't being tested nor even type-checked (unless I missed it?).

Comment on lines 24 to 25
> [!TIP]
> **Want to skip the setup?** Run `npm create @modelcontextprotocol/mcp-app my-app` to scaffold this project automatically, then skip to [Section 3: Build the View](#3-build-the-view).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually replaces all but the last step of the Quickstart guide ("See it in action").

I don't think we want to pitch this in the Quickstart, because the Quickstart is a "learn by doing" tutorial. If we replace the "doing" with npm create @modelcontextprotocol/mcp-app my-app, that kind of defeats the purpose. 😄

npm create @modelcontextprotocol/mcp-app [project-name] [options]

${pc.bold("Options:")}
-t, --template <name> Template to use (${TEMPLATES.map((t) => t.value).join(", ")})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be better named as --framework. (I would assume a --template option designates an actual template to use — i.e., a path to a directory.)


${pc.bold("Options:")}
-t, --template <name> Template to use (${TEMPLATES.map((t) => t.value).join(", ")})
--no-install Skip npm install
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious: what's the use case for this option?

Comment on lines 4 to 5
/** Current SDK version - used in generated package.json files */
export const SDK_VERSION = "0.4.1";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need a way for this to be dynamically computed or at least automatically updated.

Comment on lines 32 to 34
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i.test(name)) {
return "Project name must be lowercase alphanumeric with optional hyphens";
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason for this restriction?

Comment on lines 1 to 18
.main {
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;

width: 100%;
max-width: 425px;
box-sizing: border-box;
padding: 1rem;

> * {
margin-top: 0;
margin-bottom: 0;
}

> * + * {
margin-top: 1.5rem;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want this CSS either. It might be beneficial to include some basic styling that uses CSS variables from the host context, but I'm not sure if that would be better here or in global.css. 🤔

Comment on lines 75 to 94
useEffect(() => {
if (toolResult) {
setServerTime(extractTime(toolResult));
}
}, [toolResult]);

const handleGetTime = useCallback(async () => {
try {
console.info("Calling get-time tool...");
const result = await app.callServerTool({
name: "get-time",
arguments: {},
});
console.info("get-time result:", result);
setServerTime(extractTime(result));
} catch (e) {
console.error(e);
setServerTime("[ERROR]");
}
}, [app]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to my comment about the tool, I don't think we should be including this in a scaffold.

Comment on lines +15 to +35
"@modelcontextprotocol/sdk": "^1.24.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.0",
"@types/node": "^22.0.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^4.3.4",
"concurrently": "^9.2.1",
"cross-env": "^10.1.0",
"esbuild": "^0.25.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vite": "^6.0.0",
"vite-plugin-singlefile": "^2.3.0"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wary about hard-coding these version numbers.

Comment on lines 4 to 19
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "server.ts"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

include is missing main.ts, but also there is a problem with this approach: using DOM APIs in server code isn't flagged by the IDE. We kind of accept / overlook that for our in-repo examples, but if we're generating code for other people's projects, I would like to do better.

Then test with the basic-host:

```bash
SERVERS='["http://localhost:3001/mcp"]' npx @modelcontextprotocol/basic-host
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@modelcontextprotocol/basic-host is not a published package.

@liady liady marked this pull request as draft January 29, 2026 20:18
liady and others added 2 commits January 29, 2026 22:23
- Rename --template flag to --framework
- Remove --no-install flag (always install deps)
- Read SDK_VERSION dynamically from package.json at runtime
- Simplify project name validation (filesystem + npm rules only)
- Replace get-time example tool with minimal hello stub
- Remove __dirname/import.meta.filename hack in server template
- Strip scaffold CSS to minimal layout-only styles
- Simplify React scaffold to minimal connected component
- Fix tsconfig.json to include main.ts in both templates
- Add tsconfig.server.json main.ts include for server code
- Fix README.md (remove --no-install, basic-host reference)
- Remove quickstart.md scaffold tip (tutorial shouldn't shortcut itself)
- Add E2E scaffold test that builds both templates end-to-end

Co-Authored-By: Claude Opus 4.5 <[email protected]>

function run(cmd, cwd) {
console.log(` $ ${cmd}`);
execSync(cmd, { cwd, stdio: "inherit", timeout: TIMEOUT });

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium test

This shell command depends on an uncontrolled
absolute path
.

Copilot Autofix

AI 7 minutes ago

In general, to fix this kind of issue you should avoid passing a single, interpolated string to execSync (which invokes a shell). Instead, call execFileSync/spawnSync (or execSync with an array form) with the executable name and its arguments provided as separate items. This prevents shell interpretation of spaces and metacharacters in paths or other dynamic values.

For this file, the best minimal change is:

  • Replace the run(cmd, cwd) helper so that it takes the program (file) and its argument list (args) separately, and uses execFileSync instead of execSync.
  • Update the two call sites:
    • The node ...dist/index.js ... invocation should be changed from a single interpolated string to file: "node" and an args array: [path.join(createMcpAppDir, "dist", "index.js"), projectName, "--framework", template].
    • The npm run build invocation should be changed to file: "npm" and args: ["run", "build"].
  • Keep logging readable by printing a reconstructed shell-like string purely for display, not for execution.

All needed imports are already present (execSync is currently imported). We will replace that import with execFileSync from node:child_process. All changes occur in packages/create-mcp-app/test/scaffold-build.test.mjs within the shown code.

Suggested changeset 1
packages/create-mcp-app/test/scaffold-build.test.mjs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/create-mcp-app/test/scaffold-build.test.mjs b/packages/create-mcp-app/test/scaffold-build.test.mjs
--- a/packages/create-mcp-app/test/scaffold-build.test.mjs
+++ b/packages/create-mcp-app/test/scaffold-build.test.mjs
@@ -2,7 +2,7 @@
  * End-to-end test: scaffolds each template, runs `npm install` and `npm run build`.
  * Verifies that generated code compiles without errors.
  */
-import { execSync } from "node:child_process";
+import { execFileSync } from "node:child_process";
 import fs from "node:fs";
 import os from "node:os";
 import path from "node:path";
@@ -16,9 +16,9 @@
   "..",
 );
 
-function run(cmd, cwd) {
-  console.log(`  $ ${cmd}`);
-  execSync(cmd, { cwd, stdio: "inherit", timeout: TIMEOUT });
+function run(file, args, cwd) {
+  console.log(`  $ ${file} ${args.join(" ")}`);
+  execFileSync(file, args, { cwd, stdio: "inherit", timeout: TIMEOUT });
 }
 
 let failed = false;
@@ -33,7 +33,8 @@
   try {
     // Scaffold using the CLI directly (built dist)
     run(
-      `node ${path.join(createMcpAppDir, "dist", "index.js")} ${projectName} --framework ${template}`,
+      "node",
+      [path.join(createMcpAppDir, "dist", "index.js"), projectName, "--framework", template],
       tmpRoot,
     );
 
@@ -46,7 +47,7 @@
     }
 
     // Build (install already happened during scaffold)
-    run("npm run build", projectDir);
+    run("npm", ["run", "build"], projectDir);
 
     // Verify dist output exists
     const distDir = path.join(projectDir, "dist");
EOF
@@ -2,7 +2,7 @@
* End-to-end test: scaffolds each template, runs `npm install` and `npm run build`.
* Verifies that generated code compiles without errors.
*/
import { execSync } from "node:child_process";
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
@@ -16,9 +16,9 @@
"..",
);

function run(cmd, cwd) {
console.log(` $ ${cmd}`);
execSync(cmd, { cwd, stdio: "inherit", timeout: TIMEOUT });
function run(file, args, cwd) {
console.log(` $ ${file} ${args.join(" ")}`);
execFileSync(file, args, { cwd, stdio: "inherit", timeout: TIMEOUT });
}

let failed = false;
@@ -33,7 +33,8 @@
try {
// Scaffold using the CLI directly (built dist)
run(
`node ${path.join(createMcpAppDir, "dist", "index.js")} ${projectName} --framework ${template}`,
"node",
[path.join(createMcpAppDir, "dist", "index.js"), projectName, "--framework", template],
tmpRoot,
);

@@ -46,7 +47,7 @@
}

// Build (install already happened during scaffold)
run("npm run build", projectDir);
run("npm", ["run", "build"], projectDir);

// Verify dist output exists
const distDir = path.join(projectDir, "dist");
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add official CLI scaffolding tool for MCP Apps

3 participants