Transfer from geelen/use-mcp

This commit is contained in:
Glen Maddern 2025-06-18 21:46:43 +10:00
commit de192f4de0
23 changed files with 9512 additions and 136 deletions

30
examples/inspector/.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.wrangler
# Playwright test results
test-results/
playwright-report/

View file

@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"printWidth": 140
}

View file

@ -0,0 +1,25 @@
# AI Chat Template - Development Guide
## Commands
- **Dev server**: `pnpm dev`
- **Build**: `pnpm build` (runs TypeScript compilation then Vite build)
- **Lint**: `pnpm lint` (ESLint)
- **Deploy**: `pnpm deploy` (builds and deploys with Wrangler)
- **Test**: `pnpm test` (Playwright E2E tests)
- **Test UI**: `pnpm test:ui` (Playwright test runner with UI)
- **Test headed**: `pnpm test:headed` (Run tests in visible browser)
## Code Style
- **Formatting**: Prettier with 2-space tabs, single quotes, no semicolons, 140 char line width
- **Imports**: Use `.tsx`/`.ts` extensions in imports, group by external/internal
- **Components**: React FC with explicit typing, PascalCase names
- **Hooks**: Custom hooks start with `use`, camelCase
- **Types**: Define interfaces in `src/types/index.ts`, use `type` for unions
- **Files**: Use PascalCase for components, camelCase for hooks/utilities
- **State**: Use proper TypeScript typing for all state variables
- **Error handling**: Use try/catch blocks with proper error propagation
- **Database**: IndexedDB with typed interfaces, async/await pattern
- **Styling**: Tailwind CSS classes, responsive design patterns
## Tech Stack
React 19, TypeScript, Vite, Tailwind CSS, Hono API, Cloudflare Workers, IndexedDB

View file

@ -0,0 +1,52 @@
# MCP Inspector
A minimal demo showcasing the `use-mcp` React hook for connecting to Model Context Protocol (MCP) servers.
## Features
- Connect to any MCP server via URL
- View available tools and their schemas
- Real-time connection status monitoring
- Debug logging for troubleshooting
- Clean, minimal UI focused on MCP functionality
## Getting Started
0. Make sure you've built the parent `use-mcp` directory at least once!
```bash
cd ../.. && pnpm build && cd -
```
Alternatively, run `pnpm dev` in the parent directory in a second terminal if you want to iterate on both the library and the example together.
1. Install dependencies:
```bash
pnpm install
```
2. Start the development server:
```bash
pnpm dev
```
3. Open your browser and navigate to the displayed local URL
4. Enter an MCP server URL to test the connection and explore available tools
## What This Demonstrates
This example shows how easy it is to integrate MCP servers into a React application using the `use-mcp` hook. The core functionality is just:
```tsx
import { useMcp } from 'use-mcp/react'
const connection = useMcp({
url: 'your-mcp-server-url',
debug: true,
autoRetry: false
})
// Access connection.state, connection.tools, connection.error, etc.
```
The `McpServers` component wraps this hook to provide a complete UI for server management and tool inspection.

View file

@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

View file

@ -0,0 +1,43 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Primary Meta Tags -->
<title>MCP Inspector - use-mcp React Hook Demo</title>
<meta name="description" content="Interactive demo of the use-mcp React hook for Model Context Protocol (MCP) servers. Connect to MCP servers, explore available tools, and execute commands using the use-mcp library." />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:site_name" content="MCP Inspector" />
<meta property="og:title" content="MCP Inspector - use-mcp React Hook Demo" />
<meta property="og:description" content="Interactive demo of the use-mcp React hook for Model Context Protocol (MCP) servers. Connect to MCP servers, explore available tools, and execute commands using the use-mcp library." />
<meta property="og:image" content="https://inspector.use-mcp.dev/use-mcp-card.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://inspector.use-mcp.dev" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="MCP Inspector - use-mcp React Hook Demo" />
<meta name="twitter:description" content="Interactive demo of the use-mcp React hook for Model Context Protocol (MCP) servers. Connect to MCP servers, explore available tools, and execute commands using the use-mcp library." />
<meta name="twitter:image" content="https://inspector.use-mcp.dev/use-mcp-card.png" />
<!-- Additional Meta Tags -->
<meta name="author" content="Glen Maddern" />
<meta name="keywords" content="use-mcp, Model Context Protocol, MCP, AI, LLM, tools, inspector, debugging, development, React hook" />
<meta name="application-name" content="MCP Inspector" />
<meta name="apple-mobile-web-app-title" content="MCP Inspector" />
<meta name="theme-color" content="#ffffff" />
<!-- Canonical URL -->
<link rel="canonical" href="https://inspector.use-mcp.dev" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -0,0 +1,41 @@
{
"name": "mcp-inspector",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && pnpm run build:only",
"build:only": "vite build",
"lint": "eslint .",
"preview": "vite preview",
"deploy": "pnpm run build:only && wrangler deploy"
},
"dependencies": {
"@tailwindcss/vite": "^4.0.14",
"lucide-react": "^0.482.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.6.2",
"tailwindcss": "^4.0.14",
"use-mcp": "link:../.."
},
"devDependencies": {
"@cloudflare/vite-plugin": "^1.5.0",
"@eslint/js": "^9.21.0",
"@tailwindcss/typography": "^0.5.16",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"prettier": "^3.5.3",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0",
"wrangler": "^4.19.1"
},
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
}

3795
examples/inspector/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,37 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { McpServers } from './components/McpServers.js'
import { OAuthCallback } from './components/OAuthCallback.js'
function App() {
return (
<Router>
<Routes>
<Route path="/oauth/callback" element={<OAuthCallback />} />
<Route path="/" element={
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="w-full max-w-2xl">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">MCP Inspector</h1>
<p className="text-gray-600">
Minimal demo showcasing the{' '}
<a
href="https://github.com/modelcontextprotocol/use-mcp"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 underline font-medium transition-colors"
>
use-mcp
</a>{' '}
React hook
</p>
</div>
<McpServers />
</div>
</div>
} />
</Routes>
</Router>
)
}
export default App

View file

@ -0,0 +1,557 @@
import { useState, useRef, useEffect } from 'react'
import { useMcp, type Tool } from 'use-mcp/react'
import { Info, X, ChevronRight, ChevronDown } from 'lucide-react'
// MCP Connection wrapper that only renders when active
function McpConnection({
serverUrl,
onConnectionUpdate,
}: {
serverUrl: string
onConnectionUpdate: (data: any) => void
}) {
// Use the MCP hook with the server URL
const connection = useMcp({
url: serverUrl,
debug: true,
autoRetry: false,
popupFeatures: 'width=500,height=600,resizable=yes,scrollbars=yes',
})
// Update parent component with connection data
useEffect(() => {
onConnectionUpdate(connection)
}, [
connection.state,
connection.tools,
connection.error,
connection.log.length,
connection.authUrl,
])
// Return null as this is just a hook wrapper
return null
}
export function McpServers({
onToolsUpdate,
}: {
onToolsUpdate?: (tools: Tool[]) => void
}) {
const [serverUrl, setServerUrl] = useState(() => {
return sessionStorage.getItem('mcpServerUrl') || ''
})
const [isActive, setIsActive] = useState(false)
const [connectionData, setConnectionData] = useState<any>({
state: 'not-connected',
tools: [],
error: undefined,
log: [],
authUrl: undefined,
retry: () => {},
disconnect: () => {},
authenticate: () => Promise.resolve(undefined),
callTool: (_name: string, _args?: Record<string, unknown>) =>
Promise.resolve(undefined),
clearStorage: () => {},
})
const [toolForms, setToolForms] = useState<Record<string, Record<string, any>>>({})
const [toolExecutionLogs, setToolExecutionLogs] = useState<Record<string, string>>({})
const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>({})
const logRef = useRef<HTMLDivElement>(null)
const executionLogRefs = useRef<Record<string, HTMLTextAreaElement | null>>({})
// Extract connection properties
const { state, tools, log, authUrl, disconnect, authenticate } =
connectionData
// Notify parent component when tools change
useEffect(() => {
if (onToolsUpdate && tools.length > 0) {
onToolsUpdate(
tools.map((t: Tool) => ({
...t,
callTool: (args: Record<string, unknown>) =>
connectionData.callTool(t.name, args),
})),
)
}
}, [tools, onToolsUpdate])
// Handle connection
const handleConnect = () => {
if (!serverUrl.trim()) return
setIsActive(true)
}
// Handle disconnection
const handleDisconnect = () => {
disconnect()
setIsActive(false)
setConnectionData({
state: 'not-connected',
tools: [],
error: undefined,
log: [],
authUrl: undefined,
retry: () => {},
disconnect: () => {},
authenticate: () => Promise.resolve(undefined),
callTool: (_name: string, _args?: Record<string, unknown>) =>
Promise.resolve(undefined),
clearStorage: () => {},
})
}
// Handle authentication if popup was blocked
const handleManualAuth = async () => {
try {
await authenticate()
} catch (err) {
console.error('Authentication error:', err)
}
}
// Auto-scroll log to bottom
useEffect(() => {
if (logRef.current) {
logRef.current.scrollTop = logRef.current.scrollHeight
}
}, [log])
// Generate status badge based on connection state
const getStatusBadge = () => {
const baseClasses = 'px-2 py-1 rounded-full text-xs font-medium'
switch (state) {
case 'discovering':
return (
<span className={`${baseClasses} bg-blue-100 text-blue-800`}>
Discovering
</span>
)
case 'authenticating':
return (
<span className={`${baseClasses} bg-purple-100 text-purple-800`}>
Authenticating
</span>
)
case 'connecting':
return (
<span className={`${baseClasses} bg-yellow-100 text-yellow-800`}>
Connecting
</span>
)
case 'loading':
return (
<span className={`${baseClasses} bg-orange-100 text-orange-800`}>
Loading
</span>
)
case 'ready':
return (
<span className={`${baseClasses} bg-green-100 text-green-800`}>
Connected
</span>
)
case 'failed':
return (
<span className={`${baseClasses} bg-red-100 text-red-800`}>
Failed
</span>
)
case 'not-connected':
default:
return (
<span className={`${baseClasses} bg-gray-100 text-gray-800`}>
Not Connected
</span>
)
}
}
// Log the tools to console when they change (for debugging)
useEffect(() => {
if (tools.length > 0) {
console.log('MCP Tools available:', tools)
}
}, [tools])
// Initialize form data when tools change
useEffect(() => {
const newForms: Record<string, Record<string, any>> = {}
tools.forEach((tool: Tool) => {
if (tool.inputSchema && tool.inputSchema.properties) {
const formData: Record<string, any> = {}
Object.entries(tool.inputSchema.properties).forEach(([key, schema]: [string, any]) => {
// Set default values based on type
if (schema.type === 'number' || schema.type === 'integer') {
formData[key] = schema.default || 0
} else if (schema.type === 'boolean') {
formData[key] = schema.default || false
} else {
formData[key] = schema.default || ''
}
})
newForms[tool.name] = formData
}
})
setToolForms(newForms)
}, [tools])
// Handle form input changes
const handleFormChange = (toolName: string, fieldName: string, value: any) => {
setToolForms(prev => ({
...prev,
[toolName]: {
...prev[toolName],
[fieldName]: value
}
}))
}
// Helper function to clean and update execution log
const updateExecutionLog = (toolName: string, newContent: string) => {
setToolExecutionLogs(prev => {
const currentLog = prev[toolName] || ''
const updatedLog = currentLog + newContent
// Remove blank lines and rejoin
const cleanedLog = updatedLog
.split('\n')
.filter(line => line.trim() !== '')
.join('\n')
return {
...prev,
[toolName]: cleanedLog + '\n'
}
})
}
// Handle tool execution
const handleRunTool = async (tool: Tool) => {
const args = toolForms[tool.name] || {}
const argsStr = JSON.stringify(args)
// Add execution start message
const startMessage = `Calling ${tool.name}(${argsStr})\n`
updateExecutionLog(tool.name, startMessage)
try {
const result = await connectionData.callTool(tool.name, args)
const resultStr = typeof result === 'string' ? result : JSON.stringify(result, null, 2)
updateExecutionLog(tool.name, `${resultStr}\n`)
} catch (error) {
updateExecutionLog(tool.name, `Error: ${error}\n`)
}
}
// Auto-scroll execution logs to bottom when they change
useEffect(() => {
Object.keys(toolExecutionLogs).forEach(toolName => {
const textarea = executionLogRefs.current[toolName]
if (textarea) {
textarea.scrollTop = textarea.scrollHeight
}
})
}, [toolExecutionLogs])
// Clear execution log for specific tool
const clearExecutionLog = (toolName: string) => {
setToolExecutionLogs(prev => ({
...prev,
[toolName]: ''
}))
}
// Toggle tool expanded state
const toggleTool = (toolName: string) => {
setExpandedTools(prev => ({
...prev,
[toolName]: !prev[toolName]
}))
}
// Render form field based on schema
const renderFormField = (toolName: string, fieldName: string, schema: any, isRequired: boolean) => {
const value = toolForms[toolName]?.[fieldName] || ''
if (schema.type === 'number' || schema.type === 'integer') {
return (
<input
type="number"
className="w-32 p-2 pr-6 border border-gray-200 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-300 placeholder-gray-300"
value={value}
step={schema.type === 'integer' ? 1 : 'any'}
required={isRequired}
onChange={(e) => {
const newValue = e.target.value === '' ? '' :
(schema.type === 'integer' ? parseInt(e.target.value) || 0 : parseFloat(e.target.value) || 0)
handleFormChange(toolName, fieldName, newValue)
}}
/>
)
} else if (schema.type === 'boolean') {
return (
<input
type="checkbox"
className="h-4 w-4 text-blue-600 rounded focus:ring-blue-300"
checked={value}
onChange={(e) => handleFormChange(toolName, fieldName, e.target.checked)}
/>
)
} else {
// String or other text input
return (
<input
type="text"
className="w-full p-2 border border-gray-200 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-300 placeholder-gray-300"
value={value}
required={isRequired}
placeholder={schema.description || ''}
onChange={(e) => handleFormChange(toolName, fieldName, e.target.value)}
/>
)
}
}
return (
<section className="rounded-lg bg-white p-4 border border-zinc-200">
<div className="flex items-center justify-between">
<span className="text-sm font-semibold">MCP Servers</span>
{getStatusBadge()}
</div>
<p className="text-gray-500 text-xs mt-1 mb-3">
Connect to Model Context Protocol (MCP) servers to access additional AI
capabilities.
</p>
<div className="space-y-3">
<div className="flex gap-2">
<input
type="text"
className="flex-1 p-2 border border-gray-200 rounded text-sm placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-300"
placeholder="Enter MCP server URL"
value={serverUrl}
onChange={(e) => {
const newValue = e.target.value
setServerUrl(newValue)
sessionStorage.setItem('mcpServerUrl', newValue)
}}
disabled={isActive && state !== 'failed'}
/>
{state === 'ready' ||
(isActive && state !== 'not-connected' && state !== 'failed') ? (
<button
className="px-4 py-2 bg-orange-100 hover:bg-orange-200 text-orange-900 rounded text-sm font-medium whitespace-nowrap"
onClick={handleDisconnect}
>
Disconnect
</button>
) : (
<button
className="bg-blue-600 hover:bg-blue-700 text-white rounded py-2 px-4 text-sm font-medium disabled:opacity-50 whitespace-nowrap"
onClick={handleConnect}
disabled={isActive || !serverUrl.trim()}
>
Connect
</button>
)}
</div>
{/* Authentication Link if needed */}
{authUrl && (
<div className="p-3 bg-orange-50 border border-orange-200 rounded">
<p className="text-xs mb-2">
Authentication required. Please click the link below:
</p>
<a
href={authUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-orange-700 hover:text-orange-800 underline"
onClick={handleManualAuth}
>
Authenticate in new window
</a>
</div>
)}
{/* Available Tools section - always present */}
<div>
<h3 className="font-medium text-sm mb-3">
Available Tools ({tools.length})
</h3>
{tools.length === 0 ? (
<div className="border border-gray-200 rounded p-4 bg-gray-50 text-center text-gray-500 text-sm">
No tools available. Connect to an MCP server to see available tools.
</div>
) : (
<div className="border border-gray-200 rounded p-4 bg-gray-50 space-y-2">
{tools.map((tool: Tool, index: number) => {
const isExpanded = expandedTools[tool.name] || false
return (
<div key={index} className="bg-white rounded border border-gray-100 shadow-sm">
{/* Tool header - always visible */}
<div
className="flex items-center gap-2 p-3 cursor-pointer hover:bg-gray-50 transition-colors"
onClick={() => toggleTool(tool.name)}
>
{/* Toggle arrow */}
{isExpanded ? (
<ChevronDown size={16} className="text-gray-500 flex-shrink-0" />
) : (
<ChevronRight size={16} className="text-gray-500 flex-shrink-0" />
)}
{/* Tool name */}
<h4 className="font-bold text-base text-black flex-shrink-0">
{tool.name}
</h4>
{/* Tool description when collapsed */}
{!isExpanded && tool.description && (
<p className="text-gray-600 text-sm truncate ml-2">
{tool.description}
</p>
)}
</div>
{/* Expanded content */}
{isExpanded && (
<div className="px-3 pb-3 border-t border-gray-100">
{tool.description && (
<p className="text-gray-600 mb-4 text-sm leading-relaxed mt-3">
{tool.description}
</p>
)}
{/* Form for tool parameters */}
{tool.inputSchema && tool.inputSchema.properties && (
<div className="flex flex-wrap gap-3 mb-4">
{Object.entries(tool.inputSchema.properties).map(([fieldName, schema]: [string, any]) => {
const isRequired = Array.isArray(tool.inputSchema.required) && tool.inputSchema.required.includes(fieldName)
const isTextInput = schema.type !== 'number' && schema.type !== 'integer' && schema.type !== 'boolean'
return (
<div key={fieldName} className={`space-y-1 ${isTextInput ? 'w-full' : ''}`}>
<div className="flex items-center gap-2">
<label className="text-xs font-medium text-gray-700">
{fieldName}
</label>
{schema.description && (
<div className="relative group">
<Info size={12} className="text-gray-400 cursor-help" />
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-10">
{schema.description}
</div>
</div>
)}
</div>
<div>
{renderFormField(tool.name, fieldName, schema, isRequired)}
</div>
</div>
)
})}
</div>
)}
{/* Run button */}
<button
onClick={() => handleRunTool(tool)}
disabled={state !== 'ready'}
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white rounded py-2 px-4 text-sm font-medium mb-4"
>
Run
</button>
{/* Per-tool execution log - only show if there's content */}
{toolExecutionLogs[tool.name] && (
<div className="border-t border-gray-100 pt-4">
<div className="flex items-center justify-between mb-2">
<h5 className="font-medium text-sm text-gray-700">Execution Log</h5>
<button
onClick={() => clearExecutionLog(tool.name)}
className="flex items-center gap-1 text-xs text-gray-500 hover:text-gray-700"
>
<X size={12} />
Clear
</button>
</div>
<textarea
ref={(el) => { executionLogRefs.current[tool.name] = el }}
value={toolExecutionLogs[tool.name]}
readOnly
className="w-full h-32 p-2 border border-gray-200 rounded text-[10px] font-mono bg-gray-50 resize-none placeholder-gray-300"
placeholder="Tool execution results will appear here..."
/>
</div>
)}
</div>
)}
</div>
)
})}
</div>
)}
</div>
{/* Debug Log */}
<div>
<label className="font-medium text-xs block mb-2">Debug Log</label>
<div
ref={logRef}
className="border border-gray-200 rounded p-2 bg-gray-50 h-32 overflow-y-auto font-mono text-xs"
>
{log.length > 0 ? (
log.map((entry: any, index: number) => (
<div
key={index}
className={`py-0.5 ${
entry.level === 'debug'
? 'text-gray-500'
: entry.level === 'info'
? 'text-blue-600'
: entry.level === 'warn'
? 'text-orange-600'
: 'text-red-600'
}`}
>
[{entry.level}] {entry.message}
</div>
))
) : (
<div className="text-gray-400">No log entries yet</div>
)}
</div>
{connectionData?.state !== 'not-connected' && (
<button
onClick={() => {
connectionData?.clearStorage()
if (isActive) {
handleDisconnect()
}
}}
className="text-xs text-orange-600 hover:text-orange-800 hover:underline mt-2"
>
Clear stored authentication
</button>
)}
</div>
</div>
{/* Only render the actual MCP connection when active */}
{isActive && (
<McpConnection
serverUrl={serverUrl}
onConnectionUpdate={setConnectionData}
/>
)}
</section>
)
}

View file

@ -0,0 +1,18 @@
import { useEffect } from 'react'
import { onMcpAuthorization } from 'use-mcp'
export function OAuthCallback() {
useEffect(() => {
onMcpAuthorization()
}, [])
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4">Authenticating...</h1>
<p className="text-gray-600 mb-2">Please wait while we complete your authentication.</p>
<p className="text-sm text-gray-500">This window should close automatically.</p>
</div>
</div>
)
}

View file

@ -0,0 +1,3 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@config "../tailwind.config.js";

View file

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View file

@ -0,0 +1,119 @@
.light {
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em;
}
code.hljs {
padding: 3px 5px;
}
/*!
Theme: GitHub
Description: Light theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-light
Current colors taken from GitHub's CSS
*/
.hljs {
color: #24292e;
background: #ffffff;
}
.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-template-tag,
.hljs-template-variable,
.hljs-type,
.hljs-variable.language_ {
/* prettylights-syntax-keyword */
color: #d73a49;
}
.hljs-title,
.hljs-title.class_,
.hljs-title.class_.inherited__,
.hljs-title.function_ {
/* prettylights-syntax-entity */
color: #6f42c1;
}
.hljs-attr,
.hljs-attribute,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-operator,
.hljs-variable,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id {
/* prettylights-syntax-constant */
color: #005cc5;
}
.hljs-regexp,
.hljs-string,
.hljs-meta .hljs-string {
/* prettylights-syntax-string */
color: #032f62;
}
.hljs-built_in,
.hljs-symbol {
/* prettylights-syntax-variable */
color: #e36209;
}
.hljs-comment,
.hljs-code,
.hljs-formula {
/* prettylights-syntax-comment */
color: #6a737d;
}
.hljs-name,
.hljs-quote,
.hljs-selector-tag,
.hljs-selector-pseudo {
/* prettylights-syntax-entity-tag */
color: #22863a;
}
.hljs-subst {
/* prettylights-syntax-storage-modifier-import */
color: #24292e;
}
.hljs-section {
/* prettylights-syntax-markup-heading */
color: #005cc5;
font-weight: bold;
}
.hljs-bullet {
/* prettylights-syntax-markup-list */
color: #735c0f;
}
.hljs-emphasis {
/* prettylights-syntax-markup-italic */
color: #24292e;
font-style: italic;
}
.hljs-strong {
/* prettylights-syntax-markup-bold */
color: #24292e;
font-weight: bold;
}
.hljs-addition {
/* prettylights-syntax-markup-inserted */
color: #22863a;
background-color: #f0fff4;
}
.hljs-deletion {
/* prettylights-syntax-markup-deleted */
color: #b31d28;
background-color: #ffeef0;
}
.hljs-char.escape_,
.hljs-link,
.hljs-params,
.hljs-property,
.hljs-punctuation,
.hljs-tag {
/* purposely ignored */
}
}

View file

@ -0,0 +1,37 @@
/* Light theme scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Dark theme scrollbar */
.dark ::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.dark ::-webkit-scrollbar-track {
background: #27272a; /* zinc-800 */
}
.dark ::-webkit-scrollbar-thumb {
background: #52525b; /* zinc-600 */
border-radius: 4px;
}
.dark ::-webkit-scrollbar-thumb:hover {
background: #71717a; /* zinc-500 */
}

9
examples/inspector/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
/// <reference types="vite/client" />
declare global {
interface Window {
apiKeyModalResolve?: (value: boolean) => void;
}
}
export {};

View file

@ -0,0 +1,19 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
extend: {
typography: {
DEFAULT: {
css: {
pre: {
padding: "0",
filter: "brightness(96%)",
border: "0",
backgroundColor: "transparent",
},
},
},
},
},
},
};

View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View file

@ -0,0 +1,11 @@
{
"compilerOptions": {
"noUnusedLocals": false,
"noUnusedParameters": false
},
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

Some files were not shown because too many files have changed in this diff Show more