update ui

This commit is contained in:
Fredrik Jensen 2025-12-05 22:00:38 +01:00
parent 924e78d387
commit 81ff693bf7
22 changed files with 127 additions and 116 deletions

View File

@ -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

View File

@ -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 */}
<div className="header">
<div>
<h1>📈 Stock Portfolio!!</h1>
<h1>📈 Stock Portfolio</h1>
<p className="subtitle">Timeframe: {props.timeframe} {stocks.length} stocks</p>
</div>
<div className="portfolio-summary">

View File

@ -2,7 +2,6 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MCP UI Inspector</title>
</head>

View File

@ -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 */

View File

@ -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<Tool | null>(null)
const [toolResult, setToolResult] = useState<ToolResult | null>(null)
const [isExecuting, setIsExecuting] = useState(false)
const [lastExecution, setLastExecution] = useState<{ toolName: string; params: Record<string, unknown> } | null>(null)
const handleConnect = useCallback(async () => {
if (isConnected) {
@ -57,7 +59,8 @@ function App() {
const handleExecuteTool = useCallback(async (toolName: string, params: Record<string, unknown>) => {
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 (
<div className="app">
<header className="header">
<div className="header-brand">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M2 17L12 22L22 17" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
<div className="header-brand" onClick={() => window.location.reload()}>
<h1>MCP UI Inspector</h1>
</div>
<div className="header-status">
@ -127,6 +131,7 @@ function App() {
<ResultsPane
result={toolResult}
isExecuting={isExecuting}
onReload={lastExecution ? handleReload : undefined}
/>
</div>
</main>

View File

@ -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;

View File

@ -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);

View File

@ -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<Tab>('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 (
<div className={`results-pane ${isFullscreen ? 'fullscreen' : ''}`}>
<div className="panel-header">
<div className="results-tabs">
<Button
<button
className={`tab ${activeTab === 'ui' ? 'active' : ''}`}
onClick={() => setActiveTab('ui')}
icon={<Monitor size={14} />}
>
UI Output
{hasUI && <span className="tab-badge"></span>}
</Button>
<Button
<Monitor size={14} />
UI
<span className={`tab-badge ${hasUI ? 'active' : ''}`} />
</button>
<button
className={`tab ${activeTab === 'text' ? 'active' : ''}`}
onClick={() => setActiveTab('text')}
icon={<FileText size={14} />}
>
Text Response
{hasText && <span className="tab-badge"></span>}
</Button>
<FileText size={14} />
Text
<span className={`tab-badge ${hasText ? 'active' : ''}`} />
</button>
</div>
<div className="results-actions">
@ -47,6 +56,15 @@ export function ResultsPane({ result, isExecuting }: ResultsPaneProps) {
{result.timestamp.toLocaleTimeString()}
</span>
)}
{onReload && (
<Button
variant="ghost"
onClick={onReload}
disabled={isExecuting}
title="Reload"
icon={<RotateCw size={14} />}
/>
)}
<Button
variant="ghost"
onClick={() => setIsFullscreen(!isFullscreen)}

View File

@ -21,13 +21,12 @@
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
padding: 13px 16px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-secondary);
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
}
.sidebar-section-content {
@ -99,7 +98,9 @@
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
.sidebar-about {

View File

@ -62,7 +62,7 @@ export function Sidebar({
{isConnected && sessionId && (
<div className="session-info">
<div className="session-label">Session ID</div>
<code className="session-id">{sessionId.slice(0, 8)}...</code>
<code className="session-id">{sessionId.slice(0, 26)}...</code>
</div>
)}
</div>

View File

@ -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) {

View File

@ -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)}
>
<ChevronRight size={14} className="tool-chevron" />
{selectedTool?.name === tool.name ? <CircleDot size={14} className="tool-chevron" /> : <Circle size={14} className="tool-chevron" />}
<div className="tool-info">
<span className="tool-name">{tool.name}</span>
{tool.description && (

View File

@ -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 */

View File

@ -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<string, string>();
// 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<string> {
if (bundleCache.has(entryPath)) {
if (!isDev && bundleCache.has(entryPath)) {
return bundleCache.get(entryPath)!;
}
@ -23,6 +26,11 @@ export async function bundleComponent(entryPath: string): Promise<string> {
});
const bundledJS = result.outputFiles[0].text;
bundleCache.set(entryPath, bundledJS);
// Only cache in production mode
if (!isDev) {
bundleCache.set(entryPath, bundledJS);
}
return bundledJS;
}