feat(examples): add resources and prompts display to inspector

- Import and export Resource, ResourceTemplate, and Prompt types
- Add state management for resources and prompts in McpServers component
- Add UI sections to display available resources and prompts
- Implement readResource functionality with content display
- Implement getPrompt functionality with argument inputs
- Show resource templates with visual distinction
- Add expand/collapse UI for resources and prompts
- Display prompt messages with proper formatting

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jem 2025-06-19 18:46:06 -07:00
commit 7a923c3509
2 changed files with 269 additions and 8 deletions

View file

@ -1,6 +1,6 @@
import { useState, useRef, useEffect } from 'react'
import { useMcp, type Tool } from 'use-mcp/react'
import { Info, X, ChevronRight, ChevronDown } from 'lucide-react'
import { useMcp, type Tool, type Resource, type ResourceTemplate, type Prompt } from 'use-mcp/react'
import { Info, X, ChevronRight, ChevronDown, FileText, MessageSquare } from 'lucide-react'
// MCP Connection wrapper that only renders when active
function McpConnection({
@ -24,7 +24,16 @@ function McpConnection({
// Update parent component with connection data
useEffect(() => {
onConnectionUpdate(connection)
}, [connection.state, connection.tools, connection.error, connection.log.length, connection.authUrl])
}, [
connection.state,
connection.tools,
connection.resources,
connection.resourceTemplates,
connection.prompts,
connection.error,
connection.log.length,
connection.authUrl,
])
// Return null as this is just a hook wrapper
return null
@ -42,23 +51,38 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
const [connectionData, setConnectionData] = useState<any>({
state: 'not-connected',
tools: [],
resources: [],
resourceTemplates: [],
prompts: [],
error: undefined,
log: [],
authUrl: undefined,
retry: () => {},
disconnect: () => {},
authenticate: () => Promise.resolve(undefined),
callTool: (_name: string, _args?: Record<string, unknown>) => Promise.resolve(undefined),
callTool: (_name: string, _args?: Record<string, unknown>) =>
Promise.resolve(undefined),
listResources: () => Promise.resolve(),
readResource: (_uri: string) => Promise.resolve({ contents: [] }),
listPrompts: () => Promise.resolve(),
getPrompt: (_name: string, _args?: Record<string, string>) =>
Promise.resolve({ messages: [] }),
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 [expandedResources, setExpandedResources] = useState<Record<string, boolean>>({})
const [expandedPrompts, setExpandedPrompts] = useState<Record<string, boolean>>({})
const [resourceContents, setResourceContents] = useState<Record<string, any>>({})
const [promptResults, setPromptResults] = useState<Record<string, any>>({})
const [promptArgs, setPromptArgs] = useState<Record<string, Record<string, string>>>({})
const logRef = useRef<HTMLDivElement>(null)
const executionLogRefs = useRef<Record<string, HTMLTextAreaElement | null>>({})
// Extract connection properties
const { state, tools, log, authUrl, disconnect, authenticate } = connectionData
const { state, tools, resources, resourceTemplates, prompts, log, authUrl, disconnect, authenticate, readResource, getPrompt } =
connectionData
// Notify parent component when tools change
useEffect(() => {
@ -85,13 +109,22 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
setConnectionData({
state: 'not-connected',
tools: [],
resources: [],
resourceTemplates: [],
prompts: [],
error: undefined,
log: [],
authUrl: undefined,
retry: () => {},
disconnect: () => {},
authenticate: () => Promise.resolve(undefined),
callTool: (_name: string, _args?: Record<string, unknown>) => Promise.resolve(undefined),
callTool: (_name: string, _args?: Record<string, unknown>) =>
Promise.resolve(undefined),
listResources: () => Promise.resolve(),
readResource: (_uri: string) => Promise.resolve({ contents: [] }),
listPrompts: () => Promise.resolve(),
getPrompt: (_name: string, _args?: Record<string, string>) =>
Promise.resolve({ messages: [] }),
clearStorage: () => {},
})
}
@ -460,6 +493,234 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
)}
</div>
{/* Available Resources section */}
<div>
<h3 className="font-medium text-sm mb-3">
Available Resources ({resources.length + resourceTemplates.length})
</h3>
{resources.length === 0 && resourceTemplates.length === 0 ? (
<div className="border border-gray-200 rounded p-4 bg-gray-50 text-center text-gray-500 text-sm">
No resources available. Connect to an MCP server to see available resources.
</div>
) : (
<div className="border border-gray-200 rounded p-4 bg-gray-50 space-y-2">
{/* Direct Resources */}
{resources.map((resource: Resource, index: number) => {
const isExpanded = expandedResources[resource.uri] || false
const contents = resourceContents[resource.uri]
return (
<div key={`resource-${index}`} className="bg-white rounded border border-gray-100 shadow-sm">
<div className="p-3">
<div className="flex items-start gap-2">
<button
onClick={() => setExpandedResources(prev => ({ ...prev, [resource.uri]: !isExpanded }))}
className="text-gray-500 hover:text-gray-700 mt-0.5"
>
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</button>
<div className="flex-1">
<div className="flex items-center gap-2">
<FileText size={14} className="text-gray-400" />
<h4 className="font-medium text-sm">{resource.name}</h4>
</div>
{resource.description && (
<p className="text-xs text-gray-500 mt-1">{resource.description}</p>
)}
<p className="text-xs text-gray-400 mt-1 font-mono">{resource.uri}</p>
{resource.mimeType && (
<span className="text-xs text-gray-400">({resource.mimeType})</span>
)}
</div>
</div>
{isExpanded && (
<div className="mt-3 space-y-2">
<button
onClick={async () => {
try {
const result = await readResource(resource.uri)
setResourceContents(prev => ({ ...prev, [resource.uri]: result }))
} catch (error: any) {
setResourceContents(prev => ({ ...prev, [resource.uri]: { error: error.message } }))
}
}}
className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs font-medium"
>
Read Resource
</button>
{contents && (
<div className="mt-2">
{contents.error ? (
<div className="text-xs text-red-600 p-2 border border-red-200 rounded bg-red-50">
Error: {contents.error}
</div>
) : (
<div className="space-y-2">
{contents.contents?.map((content: any, idx: number) => (
<div key={idx} className="border border-gray-200 rounded p-2">
<div className="text-xs text-gray-500 mb-1">
{content.uri} {content.mimeType && `(${content.mimeType})`}
</div>
<pre className="text-xs font-mono bg-gray-50 p-2 rounded overflow-x-auto">
{content.text || (content.blob && '[Binary content]') || '[No content]'}
</pre>
</div>
))}
</div>
)}
</div>
)}
</div>
)}
</div>
</div>
)
})}
{/* Resource Templates */}
{resourceTemplates.map((template: ResourceTemplate, index: number) => (
<div key={`template-${index}`} className="bg-white rounded border border-gray-100 shadow-sm">
<div className="p-3">
<div className="flex items-center gap-2">
<FileText size={14} className="text-blue-400" />
<h4 className="font-medium text-sm">{template.name}</h4>
<span className="text-xs text-blue-600 bg-blue-50 px-2 py-0.5 rounded">Template</span>
</div>
{template.description && (
<p className="text-xs text-gray-500 mt-1">{template.description}</p>
)}
<p className="text-xs text-gray-400 mt-1 font-mono">{template.uriTemplate}</p>
{template.mimeType && (
<span className="text-xs text-gray-400">({template.mimeType})</span>
)}
</div>
</div>
))}
</div>
)}
</div>
{/* Available Prompts section */}
<div>
<h3 className="font-medium text-sm mb-3">
Available Prompts ({prompts.length})
</h3>
{prompts.length === 0 ? (
<div className="border border-gray-200 rounded p-4 bg-gray-50 text-center text-gray-500 text-sm">
No prompts available. Connect to an MCP server to see available prompts.
</div>
) : (
<div className="border border-gray-200 rounded p-4 bg-gray-50 space-y-2">
{prompts.map((prompt: Prompt, index: number) => {
const isExpanded = expandedPrompts[prompt.name] || false
const result = promptResults[prompt.name]
const args = promptArgs[prompt.name] || {}
return (
<div key={index} className="bg-white rounded border border-gray-100 shadow-sm">
<div className="p-3">
<div className="flex items-start gap-2">
<button
onClick={() => setExpandedPrompts(prev => ({ ...prev, [prompt.name]: !isExpanded }))}
className="text-gray-500 hover:text-gray-700 mt-0.5"
>
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</button>
<div className="flex-1">
<div className="flex items-center gap-2">
<MessageSquare size={14} className="text-purple-400" />
<h4 className="font-medium text-sm">{prompt.name}</h4>
</div>
{prompt.description && (
<p className="text-xs text-gray-500 mt-1">{prompt.description}</p>
)}
</div>
</div>
{isExpanded && (
<div className="mt-3 space-y-2">
{/* Prompt Arguments */}
{prompt.arguments && prompt.arguments.length > 0 && (
<div className="space-y-2">
<h5 className="text-xs font-medium text-gray-700">Arguments:</h5>
{prompt.arguments.map((arg: any) => (
<div key={arg.name} className="space-y-1">
<label className="text-xs text-gray-600">
{arg.name}
{arg.required && <span className="text-red-500 ml-1">*</span>}
{arg.description && (
<span className="text-gray-400 ml-1">({arg.description})</span>
)}
</label>
<input
type="text"
className="w-full p-2 border border-gray-200 rounded text-sm"
value={args[arg.name] || ''}
onChange={(e) => setPromptArgs(prev => ({
...prev,
[prompt.name]: {
...prev[prompt.name],
[arg.name]: e.target.value
}
}))}
placeholder={arg.description || `Enter ${arg.name}`}
/>
</div>
))}
</div>
)}
<button
onClick={async () => {
try {
const result = await getPrompt(prompt.name, args)
setPromptResults(prev => ({ ...prev, [prompt.name]: result }))
} catch (error: any) {
setPromptResults(prev => ({ ...prev, [prompt.name]: { error: error.message } }))
}
}}
className="px-3 py-1 bg-purple-600 hover:bg-purple-700 text-white rounded text-xs font-medium"
>
Get Prompt
</button>
{result && (
<div className="mt-2">
{result.error ? (
<div className="text-xs text-red-600 p-2 border border-red-200 rounded bg-red-50">
Error: {result.error}
</div>
) : (
<div className="space-y-2">
<h5 className="text-xs font-medium text-gray-700">Messages:</h5>
{result.messages?.map((message: any, idx: number) => (
<div key={idx} className="border border-gray-200 rounded p-2">
<div className="text-xs font-medium text-gray-700 mb-1">
{message.role}:
</div>
<pre className="text-xs font-mono bg-gray-50 p-2 rounded overflow-x-auto">
{message.content.text || JSON.stringify(message.content, null, 2)}
</pre>
</div>
))}
</div>
)}
</div>
)}
</div>
)}
</div>
</div>
)
})}
</div>
)}
</div>
{/* Debug Log */}
<div>
<label className="font-medium text-xs block mb-2">Debug Log</label>

View file

@ -6,5 +6,5 @@
export { useMcp } from './useMcp.js'
export type { UseMcpOptions, UseMcpResult } from './types.js'
// Re-export core Tool type for convenience when using hook result?
export type { Tool } from '@modelcontextprotocol/sdk/types.js'
// Re-export core types for convenience when using hook result
export type { Tool, Resource, ResourceTemplate, Prompt } from '@modelcontextprotocol/sdk/types.js'