Skip to content
This repository was archived by the owner on May 21, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c1dc666
fix status dot
tonxxd Oct 14, 2025
169967e
fix: 1 transport per request
tonxxd Oct 14, 2025
cc9c03a
feat: add @radix-ui/react-switch and implement auto-connect feature
tonxxd Oct 14, 2025
cd5cff9
refactor: replace console.log with console.warn for connection status…
tonxxd Oct 14, 2025
a62549f
feat: implement connection tester and auto-switch feature in Inspecto…
tonxxd Oct 14, 2025
8b2e738
Merge main into feat/auto-connect-disable
tonxxd Oct 14, 2025
2e4fea2
fix: update favicon links and improve document title in inspector ind…
tonxxd Oct 15, 2025
d01f280
fix: improve error logging for package version retrieval and update d…
tonxxd Oct 15, 2025
0aa0286
feat: enhance create-mcp-use-app with template selection and ora inte…
tonxxd Oct 15, 2025
ec76cff
feat: add test_app configuration and enhance CLI error handling
tonxxd Oct 15, 2025
1a08635
feat: update dependencies and enhance CLI functionality
tonxxd Oct 15, 2025
625ad21
feat: add terminal-link dependency and enhance CLI output
tonxxd Oct 15, 2025
213b647
feat: refactor ChatTab and Layout components for improved message han…
tonxxd Oct 15, 2025
2db29c6
Merge remote-tracking branch 'origin/feat/auto-connect-disable' into …
tonxxd Oct 15, 2025
36eab1d
feat: update pnpm-lock.yaml and enhance dev-server for widget handling
tonxxd Oct 15, 2025
d07034c
refactor: remove debug logging from dev-server for cleaner request ha…
tonxxd Oct 15, 2025
5c3c79c
feat: enhance ChatTab and ResourcesTab components with new features a…
tonxxd Oct 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "@mcp-use/cli",
"version": "2.1.9",
"type": "module",
"description": "Build tool for MCP UI widgets - bundles React components into standalone HTML pages for Model Context Protocol servers",
"author": "mcp-use, Inc.",
"license": "MIT",
Expand Down Expand Up @@ -36,17 +37,20 @@
"dev": "tsc --watch"
},
"dependencies": {
"mcp-use": "workspace:*",
"@mcp-use/inspector": "workspace:*",
"commander": "^11.0.0",
"esbuild": "^0.19.0",
"globby": "^14.0.0",
"mcp-use": "workspace:*",
"open": "^10.0.0",
"terminal-link": "^5.0.0",
"tsx": "^4.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
"@vitejs/plugin-react": "^5.0.4",
"typescript": "^5.0.0",
"vite": "^7.1.10"
},
"publishConfig": {
"access": "public"
Expand Down
91 changes: 9 additions & 82 deletions packages/cli/src/build.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { promises as fs } from 'node:fs'
import path from 'node:path'
import { build, context } from 'esbuild'
import { build } from 'esbuild'
import { globby } from 'globby'

const ROUTE_PREFIX = '/mcp-use/widgets'
Expand Down Expand Up @@ -97,7 +97,7 @@ async function buildWidget(entry: string, projectPath: string, minify = true) {
return { baseName, route }
}

export async function buildWidgets(projectPath: string, watch = false) {
export async function buildWidgets(projectPath: string) {
const srcDir = path.join(projectPath, SRC_DIR)
const outDir = path.join(projectPath, OUT_DIR)

Expand All @@ -107,88 +107,15 @@ export async function buildWidgets(projectPath: string, watch = false) {
// Find all TSX entries
const entries = await globby([`${srcDir}/**/*.tsx`])

if (!watch) {
console.log(`Building ${entries.length} widget files...`)
}
console.log(`Building ${entries.length} widget files...`)

if (watch) {
// Watch mode - create contexts for each entry but don't log individual watching messages
const contexts = []

for (const entry of entries) {
const relativePath = path.relative(projectPath, entry)
const route = toRoute(relativePath)
const pageOutDir = path.join(projectPath, outDirForRoute(route))
const baseName = path.parse(entry).name

const ctx = await context({
entryPoints: [entry],
bundle: true,
splitting: true,
format: 'esm',
platform: 'browser',
target: 'es2018',
sourcemap: true,
minify: false,
outdir: path.join(pageOutDir, 'assets'),
logLevel: 'silent',
loader: {
'.svg': 'file',
'.png': 'file',
'.jpg': 'file',
'.jpeg': 'file',
'.gif': 'file',
'.css': 'css',
},
entryNames: `[name]-[hash]`,
chunkNames: `chunk-[hash]`,
assetNames: `asset-[hash]`,
define: {
'process.env.NODE_ENV': '"development"',
},
plugins: [{
name: 'html-writer',
setup(buildPlugin) {
buildPlugin.onEnd(async () => {
try {
const files = await fs.readdir(path.join(pageOutDir, 'assets'))
const mainJs = files.find(f => f.startsWith(`${baseName}-`) && f.endsWith('.js'))
if (mainJs) {
await fs.mkdir(pageOutDir, { recursive: true })
await fs.writeFile(
path.join(pageOutDir, 'index.html'),
htmlTemplate({
title: baseName,
scriptPath: `./assets/${mainJs}`,
}),
'utf8',
)
}
} catch (err) {
console.error(`Error writing HTML for ${baseName}:`, err)
}
})
},
}],
})

contexts.push(ctx)
}

// Start watching all contexts
for (const ctx of contexts) {
await ctx.watch()
}
}
else {
// Build once
for (const entry of entries) {
const { baseName, route } = await buildWidget(entry, projectPath)
console.log(`\x1b[32m✓\x1b[0m Built ${baseName} -> ${route}`)
}

console.log('Build complete!')
// Build once (production mode)
for (const entry of entries) {
const { baseName, route } = await buildWidget(entry, projectPath)
console.log(`\x1b[32m✓\x1b[0m Built ${baseName} -> ${route}`)
}

console.log('Build complete!')
}


120 changes: 120 additions & 0 deletions packages/cli/src/dev-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { createServer, type ViteDevServer } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'node:path'
import { promises as fs } from 'node:fs'
import { globby } from 'globby'

const ROUTE_PREFIX = '/mcp-use/widgets'
const SRC_DIR = 'resources'

interface WidgetEntry {
name: string
path: string
route: string
}

async function discoverWidgets(projectPath: string): Promise<WidgetEntry[]> {
const srcDir = path.join(projectPath, SRC_DIR)
const entries = await globby([`${srcDir}/**/*.tsx`])

return entries.map(entry => {
const relativePath = path.relative(path.join(projectPath, SRC_DIR), entry)
const name = path.parse(entry).name
const route = `${ROUTE_PREFIX}/${relativePath.replace(/\.tsx?$/, '')}`

return { name, path: entry, route }
})
}

function htmlTemplate(scriptPath: string, title: string) {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>${title} Widget</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: #f5f5f5;
}
#widget-root {
max-width: 1200px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id="widget-root"></div>
<script type="module" src="${scriptPath}"></script>
</body>
</html>`
}

export async function startDevServer(projectPath: string, port: number = 5173): Promise<{ server: ViteDevServer; port: number }> {
const widgets = await discoverWidgets(projectPath)

const server = await createServer({
root: projectPath,
server: {
port,
strictPort: false,
cors: true,
hmr: {
overlay: true,
},
},
plugins: [
react(),
{
name: 'mcp-widget-html',
apply: 'serve',
configureServer(server) {
// Serve widget HTML pages
server.middlewares.use(async (req, res, next) => {
// Early check before processing
if (!req.url?.startsWith(ROUTE_PREFIX)) {
return next()
}

// Find matching widget (strip query string for matching)
const urlWithoutQuery = req.url?.split('?')[0]
const widget = widgets.find(w => urlWithoutQuery === w.route)

if (!widget) {
return next()
}

// Serve HTML with the widget entry point
const relativeWidgetPath = '/' + path.relative(projectPath, widget.path)
const html = htmlTemplate(relativeWidgetPath, widget.name)

// Transform HTML to inject Vite's HMR client
const transformedHtml = await server.transformIndexHtml(req.url, html)

res.setHeader('Content-Type', 'text/html')
res.end(transformedHtml)
})
},
},
],
optimizeDeps: {
include: ['react', 'react-dom', 'react/jsx-runtime'],
},
})

await server.listen()

const actualPort = server.config.server.port || port

console.log(`\x1b[32m✓\x1b[0m Vite dev server with HMR started on port ${actualPort}`)
console.log(`\x1b[90m ${widgets.length} widget(s) available:\x1b[0m`)
for (const widget of widgets) {
console.log(`\x1b[90m - http://localhost:${actualPort}${widget.route}\x1b[0m`)
}

return { server, port: actualPort }
}

Loading