-
Notifications
You must be signed in to change notification settings - Fork 107
feat: add @modelcontextprotocol/create-mcp-app package #353
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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]>
@modelcontextprotocol/ext-apps
@modelcontextprotocol/server-arcade
@modelcontextprotocol/server-basic-react
@modelcontextprotocol/server-basic-vanillajs
@modelcontextprotocol/server-budget-allocator
@modelcontextprotocol/server-cohort-heatmap
@modelcontextprotocol/server-customer-segmentation
@modelcontextprotocol/server-map
@modelcontextprotocol/server-pdf
@modelcontextprotocol/server-scenario-modeler
@modelcontextprotocol/server-shadertoy
@modelcontextprotocol/server-sheet-music
@modelcontextprotocol/server-system-monitor
@modelcontextprotocol/server-threejs
@modelcontextprotocol/server-transcript
@modelcontextprotocol/server-video-resource
@modelcontextprotocol/server-wiki-explorer
commit: |
- 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]>
jonathanhefner
left a comment
There was a problem hiding this 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?).
docs/quickstart.md
Outdated
| > [!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). |
There was a problem hiding this comment.
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. 😄
packages/create-mcp-app/src/cli.ts
Outdated
| npm create @modelcontextprotocol/mcp-app [project-name] [options] | ||
|
|
||
| ${pc.bold("Options:")} | ||
| -t, --template <name> Template to use (${TEMPLATES.map((t) => t.value).join(", ")}) |
There was a problem hiding this comment.
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.)
packages/create-mcp-app/src/cli.ts
Outdated
|
|
||
| ${pc.bold("Options:")} | ||
| -t, --template <name> Template to use (${TEMPLATES.map((t) => t.value).join(", ")}) | ||
| --no-install Skip npm install |
There was a problem hiding this comment.
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?
packages/create-mcp-app/src/utils.ts
Outdated
| /** Current SDK version - used in generated package.json files */ | ||
| export const SDK_VERSION = "0.4.1"; |
There was a problem hiding this comment.
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.
packages/create-mcp-app/src/utils.ts
Outdated
| if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i.test(name)) { | ||
| return "Project name must be lowercase alphanumeric with optional hyphens"; | ||
| } |
There was a problem hiding this comment.
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?
| .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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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. 🤔
| 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]); |
There was a problem hiding this comment.
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.
| "@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" |
There was a problem hiding this comment.
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.
| "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"] |
There was a problem hiding this comment.
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.
packages/create-mcp-app/README.md
Outdated
| Then test with the basic-host: | ||
|
|
||
| ```bash | ||
| SERVERS='["http://localhost:3001/mcp"]' npx @modelcontextprotocol/basic-host |
There was a problem hiding this comment.
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.
- 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
absolute path
Show autofix suggestion
Hide autofix suggestion
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 usesexecFileSyncinstead ofexecSync. - Update the two call sites:
- The
node ...dist/index.js ...invocation should be changed from a single interpolated string tofile: "node"and anargsarray:[path.join(createMcpAppDir, "dist", "index.js"), projectName, "--framework", template]. - The
npm run buildinvocation should be changed tofile: "npm"andargs: ["run", "build"].
- The
- 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.
-
Copy modified line R5 -
Copy modified lines R19-R21 -
Copy modified lines R36-R37 -
Copy modified line R50
| @@ -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"); |
Closes #354
Summary
Enable scaffolding new MCP App projects with a single command:
Features
tsxfor running the dev server (broader compatibility than bun)esbuildfor bundling server files--framework react|vanillajs- Skip framework selection promptnpm installafter scaffoldingPackage Structure
Changes
packages/create-mcp-app/with CLI implementation and templatespackages/*to workspaces in rootpackage.jsonpublish-packagesjob to npm-publish workflowTesting
Test plan
🤖 Generated with Claude Code