Skip to content
This repository was archived by the owner on May 21, 2026. It is now read-only.

Commit 3871175

Browse files
committed
feat: cli build command
1 parent 8e0a30d commit 3871175

4 files changed

Lines changed: 416 additions & 1 deletion

File tree

packages/cli/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
"dependencies": {
1414
"mcp-use-ts": "workspace:*",
1515
"@mcp-use/inspector": "workspace:*",
16-
"commander": "^11.0.0"
16+
"commander": "^11.0.0",
17+
"esbuild": "^0.19.0",
18+
"globby": "^14.0.0"
1719
},
1820
"devDependencies": {
1921
"@types/node": "^20.0.0",

packages/cli/src/build.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { promises as fs } from 'node:fs'
2+
import path from 'node:path'
3+
import { build } from 'esbuild'
4+
import { globby } from 'globby'
5+
6+
const ROUTE_PREFIX = '/mcp-use/widgets'
7+
const SRC_DIR = 'resources'
8+
const OUT_DIR = 'dist/resources'
9+
10+
function toRoute(file: string) {
11+
const rel = file.replace(new RegExp(`^${SRC_DIR}/`), '').replace(/\.tsx?$/, '')
12+
return `${ROUTE_PREFIX}/${rel}`
13+
}
14+
15+
function outDirForRoute(route: string) {
16+
return path.join(OUT_DIR, route.replace(/^\//, ''))
17+
}
18+
19+
function htmlTemplate({ title, scriptPath }: { title: string, scriptPath: string }) {
20+
return `<!doctype html>
21+
<html lang="en">
22+
<head>
23+
<meta charset="UTF-8" />
24+
<meta name="viewport" content="width=device-width,initial-scale=1" />
25+
<title>${title} Widget</title>
26+
<style>
27+
body {
28+
margin: 0;
29+
padding: 20px;
30+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
31+
background: #f5f5f5;
32+
}
33+
#widget-root {
34+
max-width: 1200px;
35+
margin: 0 auto;
36+
}
37+
</style>
38+
</head>
39+
<body>
40+
<div id="widget-root"></div>
41+
<script type="module" src="${scriptPath}"></script>
42+
</body>
43+
</html>`
44+
}
45+
46+
export async function buildWidgets(projectPath: string) {
47+
console.log('🔨 Building UI widgets with esbuild...')
48+
49+
const srcDir = path.join(projectPath, SRC_DIR)
50+
const outDir = path.join(projectPath, OUT_DIR)
51+
52+
// Clean dist
53+
await fs.rm(outDir, { recursive: true, force: true })
54+
55+
// Find all TSX entries
56+
const entries = await globby([`${srcDir}/**/*.tsx`])
57+
console.log(`📦 Found ${entries.length} widget files`)
58+
59+
// Build each entry as an isolated page with hashed output
60+
for (const entry of entries) {
61+
const relativePath = path.relative(projectPath, entry)
62+
const route = toRoute(relativePath)
63+
const pageOutDir = path.join(projectPath, outDirForRoute(route))
64+
const baseName = path.parse(entry).name
65+
66+
console.log(`🔨 Building ${baseName}...`)
67+
68+
// Build JS/CSS chunks for this page
69+
await build({
70+
entryPoints: [entry],
71+
bundle: true,
72+
splitting: true,
73+
format: 'esm',
74+
platform: 'browser',
75+
target: 'es2018',
76+
sourcemap: false,
77+
minify: true,
78+
outdir: path.join(pageOutDir, 'assets'),
79+
logLevel: 'silent',
80+
loader: {
81+
'.svg': 'file',
82+
'.png': 'file',
83+
'.jpg': 'file',
84+
'.jpeg': 'file',
85+
'.gif': 'file',
86+
'.css': 'css',
87+
},
88+
entryNames: `[name]-[hash]`,
89+
chunkNames: `chunk-[hash]`,
90+
assetNames: `asset-[hash]`,
91+
define: {
92+
'process.env.NODE_ENV': '"production"',
93+
},
94+
})
95+
96+
// Find the main entry file name
97+
const files = await fs.readdir(path.join(pageOutDir, 'assets'))
98+
const mainJs = files.find(f => f.startsWith(`${baseName}-`) && f.endsWith('.js'))
99+
if (!mainJs)
100+
throw new Error(`Failed to locate entry JS for ${entry}`)
101+
102+
// Write an index.html that points to the entry
103+
await fs.mkdir(pageOutDir, { recursive: true })
104+
await fs.writeFile(
105+
path.join(pageOutDir, 'index.html'),
106+
htmlTemplate({
107+
title: baseName,
108+
scriptPath: `./assets/${mainJs}`,
109+
}),
110+
'utf8',
111+
)
112+
113+
console.log(`✅ Built ${baseName} -> ${route}`)
114+
}
115+
116+
console.log('🎉 Build complete!')
117+
}
118+

packages/cli/src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env node
22
import { Command } from 'commander';
3+
import { buildWidgets } from './build';
34

45
const program = new Command();
56

@@ -8,4 +9,17 @@ program
89
.description('MCP CLI tool')
910
.version('0.1.0');
1011

12+
program
13+
.command('build')
14+
.description('Build MCP UI widgets')
15+
.option('-p, --path <path>', 'Path to project directory', process.cwd())
16+
.action(async (options) => {
17+
try {
18+
await buildWidgets(options.path);
19+
} catch (error) {
20+
console.error('Build failed:', error);
21+
process.exit(1);
22+
}
23+
});
24+
1125
program.parse();

0 commit comments

Comments
 (0)