diff --git a/README.md b/README.md index 207fe40..3feeae8 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,23 @@ function MyComponent() { **`createUI(name, entryUrl)`** - Creates a UI component - `name`: Component identifier -- `entryUrl`: Path from `import.meta.resolve()` +- `entryUrl`: Path to the component entry file - Returns: `{ component(opts?) }` where opts: `{ props?, frameSize? }` +The `entryUrl` parameter accepts both formats: + +```typescript +// ESM (recommended) - using import.meta.resolve() +// Requires "type": "module" in package.json +createUI('dashboard', import.meta.resolve('./MyComponent.tsx')); + +// CommonJS - using require.resolve() or absolute paths +createUI('dashboard', require.resolve('./MyComponent.tsx')); +createUI('dashboard', path.join(__dirname, './MyComponent.tsx')); +``` + +The library automatically converts `file://` URLs to file paths, so both approaches work seamlessly. + ### UI (`@mcp-ui/library/ui`) - **`useProps(defaults)`** - Get props passed from the server diff --git a/packages/server/components/StockDashboard.tsx b/packages/demo-server/components/StockDashboard.tsx similarity index 98% rename from packages/server/components/StockDashboard.tsx rename to packages/demo-server/components/StockDashboard.tsx index 2bc2fab..a1a1abc 100644 --- a/packages/server/components/StockDashboard.tsx +++ b/packages/demo-server/components/StockDashboard.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { generateMockStockData } from './stock-utils'; -import { callTool, sendPrompt, useProps } from '../../library/ui'; +import { callTool, sendPrompt, useProps } from '@mcp-ui/library/ui'; // Types for props passed from the tool handler @@ -74,7 +74,7 @@ Please analyze this portfolio and provide recommendations.`; {/* Header */}
-

📈 Stock Portfolio!!

+

📈 Stock Portfolio

Timeframe: {props.timeframe} • {stocks.length} stocks

diff --git a/packages/server/components/WeatherDashboard.tsx b/packages/demo-server/components/WeatherDashboard.tsx similarity index 100% rename from packages/server/components/WeatherDashboard.tsx rename to packages/demo-server/components/WeatherDashboard.tsx diff --git a/packages/server/components/index.tsx b/packages/demo-server/components/index.tsx similarity index 100% rename from packages/server/components/index.tsx rename to packages/demo-server/components/index.tsx diff --git a/packages/server/components/stock-entry.tsx b/packages/demo-server/components/stock-entry.tsx similarity index 100% rename from packages/server/components/stock-entry.tsx rename to packages/demo-server/components/stock-entry.tsx diff --git a/packages/server/components/stock-utils.ts b/packages/demo-server/components/stock-utils.ts similarity index 100% rename from packages/server/components/stock-utils.ts rename to packages/demo-server/components/stock-utils.ts diff --git a/packages/server/components/styles.ts b/packages/demo-server/components/styles.ts similarity index 100% rename from packages/server/components/styles.ts rename to packages/demo-server/components/styles.ts diff --git a/packages/server/index.ts b/packages/demo-server/index.ts similarity index 100% rename from packages/server/index.ts rename to packages/demo-server/index.ts diff --git a/packages/server/package.json b/packages/demo-server/package.json similarity index 100% rename from packages/server/package.json rename to packages/demo-server/package.json diff --git a/packages/server/tsconfig.json b/packages/demo-server/tsconfig.json similarity index 100% rename from packages/server/tsconfig.json rename to packages/demo-server/tsconfig.json diff --git a/packages/inspector/index.html b/packages/inspector/index.html index 4ff631f..2d5267a 100644 --- a/packages/inspector/index.html +++ b/packages/inspector/index.html @@ -2,7 +2,6 @@ - MCP UI Inspector diff --git a/packages/inspector/src/App.css b/packages/inspector/src/App.css index 0175bb8..58c332f 100644 --- a/packages/inspector/src/App.css +++ b/packages/inspector/src/App.css @@ -17,12 +17,13 @@ .header-brand { display: flex; align-items: center; - gap: 12px; - color: var(--text-primary); + gap: 7px; + color: var(--white); + cursor: pointer; } .header-brand svg { - color: var(--accent-purple); + color: var(--white); } .header-brand h1 { @@ -67,8 +68,13 @@ } @keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } } /* Main layout */ diff --git a/packages/inspector/src/App.tsx b/packages/inspector/src/App.tsx index e30911a..b40c430 100644 --- a/packages/inspector/src/App.tsx +++ b/packages/inspector/src/App.tsx @@ -4,6 +4,7 @@ import { ToolsPanel } from './components/ToolsPanel' import { ResultsPane } from './components/ResultsPane' import { useMCP } from './hooks/useMCP' import './App.css' +import { Eye, Sparkles } from 'lucide-react' export type Tool = { name: string @@ -29,20 +30,21 @@ export type ToolResult = { function App() { const [serverUrl, setServerUrl] = useState('http://localhost:3000/mcp') - const { - isConnected, - isConnecting, - sessionId, - tools, - connect, + const { + isConnected, + isConnecting, + sessionId, + tools, + connect, disconnect, callTool, - error + error } = useMCP() const [selectedTool, setSelectedTool] = useState(null) const [toolResult, setToolResult] = useState(null) const [isExecuting, setIsExecuting] = useState(false) + const [lastExecution, setLastExecution] = useState<{ toolName: string; params: Record } | null>(null) const handleConnect = useCallback(async () => { if (isConnected) { @@ -57,7 +59,8 @@ function App() { const handleExecuteTool = useCallback(async (toolName: string, params: Record) => { setIsExecuting(true) setToolResult(null) - + setLastExecution({ toolName, params }) + try { const result = await callTool(toolName, params) setToolResult({ @@ -76,15 +79,16 @@ function App() { } }, [callTool]) + const handleReload = useCallback(async () => { + if (lastExecution) { + await handleExecuteTool(lastExecution.toolName, lastExecution.params) + } + }, [lastExecution, handleExecuteTool]) + return (
-
- - - - - +
window.location.reload()}>

MCP UI Inspector

@@ -127,6 +131,7 @@ function App() {
diff --git a/packages/inspector/src/components/Button.css b/packages/inspector/src/components/Button.css index 4c6b9bb..b362286 100644 --- a/packages/inspector/src/components/Button.css +++ b/packages/inspector/src/components/Button.css @@ -9,16 +9,17 @@ font-size: 13px; font-weight: 500; border-radius: 6px; - border: 1px solid var(--border-color); - background: var(--bg-tertiary); + background: var(--bg-black); color: var(--text-primary); cursor: pointer; transition: all 0.15s ease; white-space: nowrap; + border: none; + outline: 1px solid rgba(255, 255, 255, 0.15); } .btn:hover:not(:disabled) { - background: var(--bg-hover); + box-shadow: 0 0 10px 1px rgba(255, 255, 255, 0.3); } .btn:disabled { @@ -28,14 +29,8 @@ /* Primary Variant */ .btn-primary { - background: var(--accent-blue); - border-color: var(--accent-blue); - color: white; -} - -.btn-primary:hover:not(:disabled) { - background: #4c9aed; - border-color: #4c9aed; + background: var(--white); + color: black; } /* Ghost Variant (icon buttons) */ @@ -45,11 +40,6 @@ padding: 6px; } -.btn-ghost:hover:not(:disabled) { - background: var(--bg-hover); - border-color: transparent; -} - /* Loading State */ .btn-loading { cursor: wait; diff --git a/packages/inspector/src/components/ResultsPane.css b/packages/inspector/src/components/ResultsPane.css index 08bc852..c3eb856 100644 --- a/packages/inspector/src/components/ResultsPane.css +++ b/packages/inspector/src/components/ResultsPane.css @@ -49,13 +49,21 @@ } .tab.active { - color: var(--accent-blue); + color: var(--white); border-bottom-color: var(--accent-blue); } .tab-badge { - font-size: 10px; - color: var(--accent-green); + display: inline-block; + width: 5px; + height: 5px; + margin-left: 5px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.5); +} + +.tab-badge.active { + background: var(--accent-blue); } .results-actions { @@ -152,7 +160,9 @@ } @keyframes spin { - to { transform: rotate(360deg); } + to { + transform: rotate(360deg); + } } .ui-frame { @@ -169,7 +179,8 @@ } .text-output pre { - font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace; + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, + monospace; font-size: 13px; line-height: 1.6; color: var(--text-primary); diff --git a/packages/inspector/src/components/ResultsPane.tsx b/packages/inspector/src/components/ResultsPane.tsx index 20f972a..3f80ea9 100644 --- a/packages/inspector/src/components/ResultsPane.tsx +++ b/packages/inspector/src/components/ResultsPane.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react' -import { Monitor, FileText, Clock, AlertTriangle, Maximize2, Minimize2 } from 'lucide-react' +import { useEffect, useState } from 'react' +import { Monitor, FileText, Clock, AlertTriangle, Maximize2, Minimize2, RotateCw } from 'lucide-react' import { Button } from './Button' import type { ToolResult } from '../App' import './ResultsPane.css' @@ -7,37 +7,46 @@ import './ResultsPane.css' interface ResultsPaneProps { result: ToolResult | null isExecuting: boolean + onReload?: () => void } type Tab = 'ui' | 'text' -export function ResultsPane({ result, isExecuting }: ResultsPaneProps) { +export function ResultsPane({ result, isExecuting, onReload }: ResultsPaneProps) { const [activeTab, setActiveTab] = useState('ui') const [isFullscreen, setIsFullscreen] = useState(false) const hasUI = result?.htmlContent != null const hasText = result?.textContent != null + useEffect(() => { + if (hasUI) { + setActiveTab('ui') + } else if (hasText) { + setActiveTab('text') + } + }, [hasText, hasUI]) + return (
- - + + + Text + +
@@ -47,6 +56,15 @@ export function ResultsPane({ result, isExecuting }: ResultsPaneProps) { {result.timestamp.toLocaleTimeString()} )} + {onReload && ( +
diff --git a/packages/inspector/src/components/ToolsPanel.css b/packages/inspector/src/components/ToolsPanel.css index 266416a..b1b728f 100644 --- a/packages/inspector/src/components/ToolsPanel.css +++ b/packages/inspector/src/components/ToolsPanel.css @@ -19,7 +19,7 @@ } .tool-count { - background: var(--bg-hover); + background: rgba(255, 255, 255, 0.1); padding: 2px 8px; border-radius: 10px; font-size: 11px; @@ -165,7 +165,8 @@ .json-editor { width: 100%; - font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace; + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, + monospace; font-size: 12px; background: var(--bg-primary); border: 1px solid var(--border-color); @@ -240,7 +241,9 @@ } @keyframes spin { - to { transform: rotate(360deg); } + to { + transform: rotate(360deg); + } } @media (max-width: 1200px) { diff --git a/packages/inspector/src/components/ToolsPanel.tsx b/packages/inspector/src/components/ToolsPanel.tsx index e454139..fa748c6 100644 --- a/packages/inspector/src/components/ToolsPanel.tsx +++ b/packages/inspector/src/components/ToolsPanel.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { Wrench, Play, ChevronRight, Code2, FileJson } from 'lucide-react' +import { Wrench, Play, Circle, CircleDot, Code2, FileJson } from 'lucide-react' import { Button } from './Button' import type { Tool } from '../App' import './ToolsPanel.css' @@ -148,7 +148,7 @@ export function ToolsPanel({ className={`tool-item ${selectedTool?.name === tool.name ? 'selected' : ''}`} onClick={() => onSelectTool(tool)} > - + {selectedTool?.name === tool.name ? : }
{tool.name} {tool.description && ( diff --git a/packages/inspector/src/index.css b/packages/inspector/src/index.css index 2e1f5d1..95fafc9 100644 --- a/packages/inspector/src/index.css +++ b/packages/inspector/src/index.css @@ -8,7 +8,6 @@ --bg-primary: #000000; --bg-secondary: #000000; --bg-tertiary: #141414; - --bg-hover: #30363d; --border-color: #30363d; --text-primary: #e6edf3; --text-secondary: #8b949e; @@ -18,6 +17,7 @@ --accent-red: #f85149; --accent-orange: #d29922; --accent-purple: #a371f7; + --white: #ffffff; } body { @@ -69,51 +69,7 @@ textarea { input:focus, select:focus, textarea:focus { - border-color: var(--accent-blue); -} - -/* Button styles */ -button { - background: var(--bg-tertiary); - border: 1px solid var(--border-color); - border-radius: 6px; - color: var(--text-primary); - padding: 8px 16px; - font-size: 14px; - cursor: pointer; - transition: background 0.2s, border-color 0.2s; - display: inline-flex; - align-items: center; - gap: 8px; -} - -button:hover { - background: var(--bg-hover); -} - -button:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -button.primary { - background: var(--accent-blue); - border-color: var(--accent-blue); - color: #fff; -} - -button.primary:hover { - background: #4c9aed; -} - -button.success { - background: var(--accent-green); - border-color: var(--accent-green); - color: #fff; -} - -button.success:hover { - background: #2ea043; + border-color: var(--white); } /* Code/mono text */ diff --git a/packages/library/server/bundle.ts b/packages/library/server/bundle.ts index 7afc4f4..c203830 100644 --- a/packages/library/server/bundle.ts +++ b/packages/library/server/bundle.ts @@ -1,10 +1,13 @@ import * as esbuild from 'esbuild'; -// Cache bundled JS per entry path +// Cache bundled JS per entry path (only used in production) const bundleCache = new Map(); +// In development mode, skip cache to allow hot-reloading of component changes +const isDev = process.env.NODE_ENV !== 'production'; + export async function bundleComponent(entryPath: string): Promise { - if (bundleCache.has(entryPath)) { + if (!isDev && bundleCache.has(entryPath)) { return bundleCache.get(entryPath)!; } @@ -23,6 +26,11 @@ export async function bundleComponent(entryPath: string): Promise { }); const bundledJS = result.outputFiles[0].text; - bundleCache.set(entryPath, bundledJS); + + // Only cache in production mode + if (!isDev) { + bundleCache.set(entryPath, bundledJS); + } + return bundledJS; } \ No newline at end of file