>({})
const [jsonMode, setJsonMode] = useState(false)
@@ -62,12 +66,14 @@ export function ToolsPanel({
const props = selectedTool.inputSchema?.properties || {}
for (const [key, value] of Object.entries(params)) {
- if (!value && !props[key]?.default) continue
-
const propType = props[key]?.type
+ // Skip empty optional fields (but always include if there's a value or it's required)
+ const isRequired = selectedTool.inputSchema?.required?.includes(key)
+ if (!value && !isRequired && !props[key]?.default) continue
+
// Try to parse as JSON for arrays/objects
- if (value.startsWith('[') || value.startsWith('{')) {
+ if (value && (value.startsWith('[') || value.startsWith('{'))) {
try {
finalParams[key] = JSON.parse(value)
continue
@@ -78,7 +84,7 @@ export function ToolsPanel({
// Convert to appropriate type
if (propType === 'number' || propType === 'integer') {
- finalParams[key] = Number(value)
+ finalParams[key] = value ? Number(value) : undefined
} else if (propType === 'boolean') {
finalParams[key] = value === 'true'
} else {
@@ -128,6 +134,16 @@ export function ToolsPanel({
Tools
{tools.length}
+ {isConnected && (
+
+ )}
diff --git a/packages/inspector/src/hooks/useMCP.ts b/packages/inspector/src/hooks/useMCP.ts
index 81a08c2..3963072 100644
--- a/packages/inspector/src/hooks/useMCP.ts
+++ b/packages/inspector/src/hooks/useMCP.ts
@@ -5,10 +5,13 @@ interface UseMCPReturn {
isConnected: boolean
isConnecting: boolean
sessionId: string | null
+ isStateless: boolean
tools: Tool[]
+ isRefreshing: boolean
error: string | null
connect: (serverUrl: string) => Promise
disconnect: () => void
+ refreshTools: () => Promise
callTool: (toolName: string, params: Record) => Promise<{
textContent: string
htmlContent: string | null
@@ -43,10 +46,13 @@ export function useMCP(): UseMCPReturn {
const [isConnected, setIsConnected] = useState(false)
const [isConnecting, setIsConnecting] = useState(false)
const [sessionId, setSessionId] = useState(null)
+ const [isStateless, setIsStateless] = useState(false)
const [tools, setTools] = useState([])
+ const [isRefreshing, setIsRefreshing] = useState(false)
const [error, setError] = useState(null)
-
+
const serverUrlRef = useRef('')
+ const sessionIdRef = useRef(null)
const connect = useCallback(async (serverUrl: string) => {
setIsConnecting(true)
@@ -81,16 +87,28 @@ export function useMCP(): UseMCPReturn {
throw new Error(initResult?.error?.message || 'Failed to initialize')
}
+ // Track whether this is a stateless server (no session ID)
+ const serverIsStateless = !newSessionId
setSessionId(newSessionId)
+ sessionIdRef.current = newSessionId
+ setIsStateless(serverIsStateless)
+
+ // Helper to build headers (only include session ID if present)
+ const buildHeaders = () => {
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json, text/event-stream',
+ }
+ if (newSessionId) {
+ headers['mcp-session-id'] = newSessionId
+ }
+ return headers
+ }
// Send initialized notification
await fetch(serverUrl, {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Accept': 'application/json, text/event-stream',
- 'mcp-session-id': newSessionId || '',
- },
+ headers: buildHeaders(),
body: JSON.stringify({
jsonrpc: '2.0',
method: 'notifications/initialized',
@@ -100,11 +118,7 @@ export function useMCP(): UseMCPReturn {
// List tools
const toolsResponse = await fetch(serverUrl, {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Accept': 'application/json, text/event-stream',
- 'mcp-session-id': newSessionId || '',
- },
+ headers: buildHeaders(),
body: JSON.stringify({
jsonrpc: '2.0',
id: 2,
@@ -125,6 +139,7 @@ export function useMCP(): UseMCPReturn {
setError(err instanceof Error ? err.message : 'Connection failed')
setIsConnected(false)
setSessionId(null)
+ setIsStateless(false)
setTools([])
} finally {
setIsConnecting(false)
@@ -134,22 +149,64 @@ export function useMCP(): UseMCPReturn {
const disconnect = useCallback(() => {
setIsConnected(false)
setSessionId(null)
+ sessionIdRef.current = null
+ setIsStateless(false)
setTools([])
setError(null)
}, [])
+ const refreshTools = useCallback(async () => {
+ if (!isConnected) return
+
+ setIsRefreshing(true)
+ try {
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json, text/event-stream',
+ }
+ if (sessionIdRef.current) {
+ headers['mcp-session-id'] = sessionIdRef.current
+ }
+
+ const toolsResponse = await fetch(serverUrlRef.current, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: Date.now(),
+ method: 'tools/list',
+ params: {},
+ }),
+ })
+
+ const toolsText = await toolsResponse.text()
+ const toolsResult = parseSSE(toolsText) as { result?: { tools: Tool[] }; error?: { message: string } }
+
+ if (toolsResult?.result?.tools) {
+ setTools(toolsResult.result.tools)
+ }
+ } finally {
+ setIsRefreshing(false)
+ }
+ }, [isConnected])
+
const callTool = useCallback(async (toolName: string, params: Record) => {
- if (!sessionId) {
+ if (!isConnected) {
throw new Error('Not connected')
}
+ // Build headers - only include session ID if present (stateful server)
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json, text/event-stream',
+ }
+ if (sessionId) {
+ headers['mcp-session-id'] = sessionId
+ }
+
const response = await fetch(serverUrlRef.current, {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Accept': 'application/json, text/event-stream',
- 'mcp-session-id': sessionId,
- },
+ headers,
body: JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
@@ -195,16 +252,19 @@ export function useMCP(): UseMCPReturn {
htmlContent,
isError: false,
}
- }, [sessionId])
+ }, [isConnected, sessionId])
return {
isConnected,
isConnecting,
sessionId,
+ isStateless,
tools,
+ isRefreshing,
error,
connect,
disconnect,
+ refreshTools,
callTool,
}
}
diff --git a/packages/library/server/bundle.ts b/packages/library/server/bundle.ts
index c203830..0aa8bc1 100644
--- a/packages/library/server/bundle.ts
+++ b/packages/library/server/bundle.ts
@@ -6,12 +6,8 @@ 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 (!isDev && bundleCache.has(entryPath)) {
- return bundleCache.get(entryPath)!;
- }
-
- const result = await esbuild.build({
+async function runBuild(entryPath: string): Promise> {
+ return esbuild.build({
entryPoints: [entryPath],
bundle: true,
write: false, // No disk I/O - keep everything in memory
@@ -22,8 +18,29 @@ export async function bundleComponent(entryPath: string): Promise {
'.tsx': 'tsx',
'.ts': 'ts',
},
- minify: false,
+ minify: !isDev,
});
+}
+
+export async function bundleComponent(entryPath: string): Promise {
+ if (!isDev && bundleCache.has(entryPath)) {
+ return bundleCache.get(entryPath)!;
+ }
+
+ let result: esbuild.BuildResult<{ write: false }>;
+
+ try {
+ result = await runBuild(entryPath);
+ } catch (error) {
+ // Handle "The service was stopped" error in serverless environments (Vercel, Lambda, etc.)
+ // This happens when the serverless runtime freezes/stops the esbuild subprocess.
+ // esbuild automatically restarts its service on the next call, so we just retry.
+ if (error instanceof Error && error.message.includes('service was stopped')) {
+ result = await runBuild(entryPath);
+ } else {
+ throw error;
+ }
+ }
const bundledJS = result.outputFiles[0].text;