Merge pull request #5 from jem-computer/feat/resources-and-prompts

Feat/resources and prompts
This commit is contained in:
Glen Maddern 2025-07-03 14:12:12 +10:00 • committed by GitHub
commit 94ba6ff372
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 919 additions and 55 deletions

View file

@ -25,6 +25,9 @@ yarn add use-mcp
- 🔄 Automatic connection management with reconnection and retries - 🔄 Automatic connection management with reconnection and retries
- 🔐 OAuth authentication flow handling with popup and fallback support - 🔐 OAuth authentication flow handling with popup and fallback support
- 📦 Simple React hook interface for MCP integration - 📦 Simple React hook interface for MCP integration
- 🧰 Full support for MCP tools, resources, and prompts
- 📄 Access server resources and read their contents
- 💬 Use server-provided prompt templates
- 🧰 TypeScript types for editor assistance and type checking - 🧰 TypeScript types for editor assistance and type checking
- 📝 Comprehensive logging for debugging - 📝 Comprehensive logging for debugging
- 🌐 Works with both HTTP and SSE (Server-Sent Events) transports - 🌐 Works with both HTTP and SSE (Server-Sent Events) transports
@ -38,8 +41,12 @@ function MyAIComponent() {
const { const {
state, // Connection state: 'discovering' | 'pending_auth' | 'authenticating' | 'connecting' | 'loading' | 'ready' | 'failed' state, // Connection state: 'discovering' | 'pending_auth' | 'authenticating' | 'connecting' | 'loading' | 'ready' | 'failed'
tools, // Available tools from MCP server tools, // Available tools from MCP server
resources, // Available resources from MCP server
prompts, // Available prompts from MCP server
error, // Error message if connection failed error, // Error message if connection failed
callTool, // Function to call tools on the MCP server callTool, // Function to call tools on the MCP server
readResource, // Function to read resource contents
getPrompt, // Function to get prompt messages
retry, // Retry connection manually retry, // Retry connection manually
authenticate, // Manually trigger authentication authenticate, // Manually trigger authentication
clearStorage, // Clear stored tokens and credentials clearStorage, // Clear stored tokens and credentials
@ -83,6 +90,32 @@ function MyAIComponent() {
))} ))}
</ul> </ul>
<button onClick={handleSearch}>Search</button> <button onClick={handleSearch}>Search</button>
{/* Example: Display and read resources */}
{resources.length > 0 && (
<div>
<h3>Resources: {resources.length}</h3>
<button onClick={async () => {
const content = await readResource(resources[0].uri)
console.log('Resource content:', content)
}}>
Read First Resource
</button>
</div>
)}
{/* Example: Use prompts */}
{prompts.length > 0 && (
<div>
<h3>Prompts: {prompts.length}</h3>
<button onClick={async () => {
const result = await getPrompt(prompts[0].name)
console.log('Prompt messages:', result.messages)
}}>
Get First Prompt
</button>
</div>
)}
</div> </div>
) )
} }
@ -176,10 +209,17 @@ function useMcp(options: UseMcpOptions): UseMcpResult
|----------|------|-------------| |----------|------|-------------|
| `state` | `string` | Current connection state: 'discovering', 'pending_auth', 'authenticating', 'connecting', 'loading', 'ready', 'failed' | | `state` | `string` | Current connection state: 'discovering', 'pending_auth', 'authenticating', 'connecting', 'loading', 'ready', 'failed' |
| `tools` | `Tool[]` | Available tools from the MCP server | | `tools` | `Tool[]` | Available tools from the MCP server |
| `resources` | `Resource[]` | Available resources from the MCP server |
| `resourceTemplates` | `ResourceTemplate[]` | Available resource templates from the MCP server |
| `prompts` | `Prompt[]` | Available prompts from the MCP server |
| `error` | `string \| undefined` | Error message if connection failed | | `error` | `string \| undefined` | Error message if connection failed |
| `authUrl` | `string \| undefined` | Manual authentication URL if popup is blocked | | `authUrl` | `string \| undefined` | Manual authentication URL if popup is blocked |
| `log` | `{ level: 'debug' \| 'info' \| 'warn' \| 'error'; message: string; timestamp: number }[]` | Array of log messages | | `log` | `{ level: 'debug' \| 'info' \| 'warn' \| 'error'; message: string; timestamp: number }[]` | Array of log messages |
| `callTool` | `(name: string, args?: Record<string, unknown>) => Promise<any>` | Function to call a tool on the MCP server | | `callTool` | `(name: string, args?: Record<string, unknown>) => Promise<any>` | Function to call a tool on the MCP server |
| `listResources` | `() => Promise<void>` | Refresh the list of available resources |
| `readResource` | `(uri: string) => Promise<{ contents: Array<...> }>` | Read the contents of a specific resource |
| `listPrompts` | `() => Promise<void>` | Refresh the list of available prompts |
| `getPrompt` | `(name: string, args?: Record<string, string>) => Promise<{ messages: Array<...> }>` | Get a specific prompt with optional arguments |
| `retry` | `() => void` | Manually attempt to reconnect | | `retry` | `() => void` | Manually attempt to reconnect |
| `disconnect` | `() => void` | Disconnect from the MCP server | | `disconnect` | `() => void` | Disconnect from the MCP server |
| `authenticate` | `() => void` | Manually trigger authentication | | `authenticate` | `() => void` | Manually trigger authentication |

View file

@ -6,6 +6,8 @@ A minimal demo showcasing the `use-mcp` React hook for connecting to Model Conte
- Connect to any MCP server via URL - Connect to any MCP server via URL
- View available tools and their schemas - View available tools and their schemas
- Browse and read server resources
- Interact with server-provided prompts
- Real-time connection status monitoring - Real-time connection status monitoring
- Debug logging for troubleshooting - Debug logging for troubleshooting
- Clean, minimal UI focused on MCP functionality - Clean, minimal UI focused on MCP functionality
@ -31,7 +33,7 @@ pnpm dev
3. Open your browser and navigate to the displayed local URL 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 4. Enter an MCP server URL to test the connection and explore available tools, resources, and prompts
## What This Demonstrates ## What This Demonstrates
@ -46,7 +48,17 @@ const connection = useMcp({
autoRetry: false autoRetry: false
}) })
// Access connection.state, connection.tools, connection.error, etc. // Access connection.state, connection.tools, connection.resources,
// connection.prompts, connection.error, etc.
``` ```
The `McpServers` component wraps this hook to provide a complete UI for server management and tool inspection. The `McpServers` component wraps this hook to provide a complete UI for server management, tool inspection, resource browsing, and prompt interaction.
## Supported MCP Features
- **Tools**: Execute server-provided tools with custom arguments and view results
- **Resources**: Browse available resources and read their contents (text or binary)
- **Resource Templates**: View dynamic resource templates with URI patterns
- **Prompts**: Interact with server prompts, provide arguments, and view generated messages
Note: Not all MCP servers implement all features. The inspector will gracefully handle servers that only support a subset of the MCP specification.

View file

@ -1,6 +1,6 @@
import { useState, useRef, useEffect } from 'react' import { useState, useRef, useEffect } from 'react'
import { useMcp, type Tool } from 'use-mcp/react' import { useMcp, type Tool, type Resource, type ResourceTemplate, type Prompt } from 'use-mcp/react'
import { Info, X, ChevronRight, ChevronDown } from 'lucide-react' import { Info, X, ChevronRight, ChevronDown, FileText, MessageSquare } from 'lucide-react'
// MCP Connection wrapper that only renders when active // MCP Connection wrapper that only renders when active
function McpConnection({ function McpConnection({
@ -24,7 +24,16 @@ function McpConnection({
// Update parent component with connection data // Update parent component with connection data
useEffect(() => { useEffect(() => {
onConnectionUpdate(connection) 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 as this is just a hook wrapper
return null return null
@ -42,6 +51,9 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
const [connectionData, setConnectionData] = useState<any>({ const [connectionData, setConnectionData] = useState<any>({
state: 'not-connected', state: 'not-connected',
tools: [], tools: [],
resources: [],
resourceTemplates: [],
prompts: [],
error: undefined, error: undefined,
log: [], log: [],
authUrl: undefined, authUrl: undefined,
@ -49,16 +61,26 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
disconnect: () => {}, disconnect: () => {},
authenticate: () => Promise.resolve(undefined), 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: () => {}, clearStorage: () => {},
}) })
const [toolForms, setToolForms] = useState<Record<string, Record<string, any>>>({}) const [toolForms, setToolForms] = useState<Record<string, Record<string, any>>>({})
const [toolExecutionLogs, setToolExecutionLogs] = useState<Record<string, string>>({}) const [toolExecutionLogs, setToolExecutionLogs] = useState<Record<string, string>>({})
const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>({}) 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 logRef = useRef<HTMLDivElement>(null)
const executionLogRefs = useRef<Record<string, HTMLTextAreaElement | null>>({}) const executionLogRefs = useRef<Record<string, HTMLTextAreaElement | null>>({})
// Extract connection properties // 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 // Notify parent component when tools change
useEffect(() => { useEffect(() => {
@ -85,6 +107,9 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
setConnectionData({ setConnectionData({
state: 'not-connected', state: 'not-connected',
tools: [], tools: [],
resources: [],
resourceTemplates: [],
prompts: [],
error: undefined, error: undefined,
log: [], log: [],
authUrl: undefined, authUrl: undefined,
@ -92,6 +117,10 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
disconnect: () => {}, disconnect: () => {},
authenticate: () => Promise.resolve(undefined), 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: () => {}, clearStorage: () => {},
}) })
} }
@ -460,6 +489,218 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
)} )}
</div> </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 */} {/* Debug Log */}
<div> <div>
<label className="font-medium text-xs block mb-2">Debug Log</label> <label className="font-medium text-xs block mb-2">Debug Log</label>

View file

@ -12,7 +12,7 @@
"type-check": "tsc --noEmit" "type-check": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.13.0", "@modelcontextprotocol/sdk": "^1.13.3",
"zod": "^3.25.67" "zod": "^3.25.67"
}, },
"devDependencies": { "devDependencies": {

View file

@ -9,8 +9,8 @@ importers:
.: .:
dependencies: dependencies:
'@modelcontextprotocol/sdk': '@modelcontextprotocol/sdk':
specifier: ^1.13.0 specifier: ^1.13.3
version: 1.13.2 version: 1.13.3
zod: zod:
specifier: ^3.25.67 specifier: ^3.25.67
version: 3.25.67 version: 3.25.67
@ -443,8 +443,8 @@ packages:
'@jridgewell/trace-mapping@0.3.9': '@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@modelcontextprotocol/sdk@1.13.2': '@modelcontextprotocol/sdk@1.13.3':
resolution: {integrity: sha512-Vx7qOcmoKkR3qhaQ9qf3GxiVKCEu+zfJddHv6x3dY/9P6+uIwJnmuAur5aB+4FDXf41rRrDnOEGkviX5oYZ67w==} resolution: {integrity: sha512-bGwA78F/U5G2jrnsdRkPY3IwIwZeWUEfb5o764b79lb0rJmMT76TLwKhdNZOWakOQtedYefwIR4emisEMvInKA==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@opentelemetry/api@1.9.0': '@opentelemetry/api@1.9.0':
@ -1314,13 +1314,14 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.2 '@jridgewell/sourcemap-codec': 1.5.2
'@modelcontextprotocol/sdk@1.13.2': '@modelcontextprotocol/sdk@1.13.3':
dependencies: dependencies:
ajv: 6.12.6 ajv: 6.12.6
content-type: 1.0.5 content-type: 1.0.5
cors: 2.8.5 cors: 2.8.5
cross-spawn: 7.0.6 cross-spawn: 7.0.6
eventsource: 3.0.7 eventsource: 3.0.7
eventsource-parser: 3.0.3
express: 5.1.0 express: 5.1.0
express-rate-limit: 7.5.1(express@5.1.0) express-rate-limit: 7.5.1(express@5.1.0)
pkce-challenge: 5.0.0 pkce-challenge: 5.0.0
@ -1349,7 +1350,7 @@ snapshots:
agents@0.0.100(@cloudflare/workers-types@4.20250628.0)(react@19.1.0): agents@0.0.100(@cloudflare/workers-types@4.20250628.0)(react@19.1.0):
dependencies: dependencies:
'@modelcontextprotocol/sdk': 1.13.2 '@modelcontextprotocol/sdk': 1.13.3
ai: 4.3.16(react@19.1.0)(zod@3.25.67) ai: 4.3.16(react@19.1.0)(zod@3.25.67)
cron-schedule: 5.0.4 cron-schedule: 5.0.4
nanoid: 5.1.5 nanoid: 5.1.5

View file

@ -1,7 +1,7 @@
import { McpAgent } from 'agents/mcp' import { McpAgent } from 'agents/mcp'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { z } from 'zod' import { z } from 'zod'
import OAuthProvider, { OAuthHelpers } from '@cloudflare/workers-oauth-provider' import OAuthProvider, { type OAuthHelpers } from '@cloudflare/workers-oauth-provider'
import { Hono } from 'hono' import { Hono } from 'hono'
// Define our MCP agent with tools // Define our MCP agent with tools
@ -53,6 +53,95 @@ export class MyMCP extends McpAgent {
return { content: [{ type: 'text', text: String(result) }] } return { content: [{ type: 'text', text: String(result) }] }
}, },
) )
// Register sample resources using the agents framework API
this.server.resource('calc://history', 'Calculation History', async () => ({
contents: [
{
uri: 'calc://history',
mimeType: 'application/json',
text: JSON.stringify(
{
calculations: [
{ operation: 'add', a: 5, b: 3, result: 8, timestamp: '2024-01-01T10:00:00Z' },
{ operation: 'multiply', a: 4, b: 7, result: 28, timestamp: '2024-01-01T10:01:00Z' },
],
},
null,
2,
),
},
],
}))
this.server.resource('calc://settings', 'Calculator Settings', async () => ({
contents: [
{
uri: 'calc://settings',
mimeType: 'application/json',
text: JSON.stringify(
{
precision: 2,
allowNegative: true,
maxValue: 1000000,
},
null,
2,
),
},
],
}))
// Register a dynamic resource
this.server.resource('calc://stats', 'Calculation Statistics', async () => ({
contents: [
{
uri: 'calc://stats',
mimeType: 'application/json',
text: JSON.stringify(
{
totalCalculations: Math.floor(Math.random() * 100),
lastUpdated: new Date().toISOString(),
},
null,
2,
),
},
],
}))
// Register sample prompts using the agents framework API
this.server.prompt(
'math_problem',
'Generate a math problem',
{
difficulty: z.string(),
topic: z.string().optional(),
},
async ({ difficulty, topic }) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Generate a ${difficulty} math problem${topic ? ` about ${topic}` : ''}`,
},
},
],
}),
)
this.server.prompt('explain_calculation', 'Explain a calculation', { operation: z.string() }, async ({ operation }) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Please explain how to perform ${operation} step by step with examples`,
},
},
],
}))
} }
} }

View file

@ -10,7 +10,7 @@
}, },
"devDependencies": { "devDependencies": {
"@hono/mcp": "^0.1.0", "@hono/mcp": "^0.1.0",
"@modelcontextprotocol/sdk": "^1.13.1", "@modelcontextprotocol/sdk": "^1.13.3",
"wrangler": "^4.21.0", "wrangler": "^4.21.0",
"zod": "^3.25.67" "zod": "^3.25.67"
} }

View file

@ -14,10 +14,10 @@ importers:
devDependencies: devDependencies:
'@hono/mcp': '@hono/mcp':
specifier: ^0.1.0 specifier: ^0.1.0
version: 0.1.0(@modelcontextprotocol/sdk@1.13.1)(hono@4.8.2) version: 0.1.0(@modelcontextprotocol/sdk@1.13.3)(hono@4.8.2)
'@modelcontextprotocol/sdk': '@modelcontextprotocol/sdk':
specifier: ^1.13.1 specifier: ^1.13.3
version: 1.13.1 version: 1.13.3
wrangler: wrangler:
specifier: ^4.21.0 specifier: ^4.21.0
version: 4.21.0 version: 4.21.0
@ -352,8 +352,8 @@ packages:
'@jridgewell/trace-mapping@0.3.9': '@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@modelcontextprotocol/sdk@1.13.1': '@modelcontextprotocol/sdk@1.13.3':
resolution: {integrity: sha512-8q6+9aF0yA39/qWT/uaIj6zTpC+Qu07DnN/lb9mjoquCJsAh6l3HyYqc9O3t2j7GilseOQOQimLg7W3By6jqvg==} resolution: {integrity: sha512-bGwA78F/U5G2jrnsdRkPY3IwIwZeWUEfb5o764b79lb0rJmMT76TLwKhdNZOWakOQtedYefwIR4emisEMvInKA==}
engines: {node: '>=18'} engines: {node: '>=18'}
accepts@2.0.0: accepts@2.0.0:
@ -972,9 +972,9 @@ snapshots:
'@fastify/busboy@2.1.1': {} '@fastify/busboy@2.1.1': {}
'@hono/mcp@0.1.0(@modelcontextprotocol/sdk@1.13.1)(hono@4.8.2)': '@hono/mcp@0.1.0(@modelcontextprotocol/sdk@1.13.3)(hono@4.8.2)':
dependencies: dependencies:
'@modelcontextprotocol/sdk': 1.13.1 '@modelcontextprotocol/sdk': 1.13.3
hono: 4.8.2 hono: 4.8.2
'@img/sharp-darwin-arm64@0.33.5': '@img/sharp-darwin-arm64@0.33.5':
@ -1061,13 +1061,14 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
'@modelcontextprotocol/sdk@1.13.1': '@modelcontextprotocol/sdk@1.13.3':
dependencies: dependencies:
ajv: 6.12.6 ajv: 6.12.6
content-type: 1.0.5 content-type: 1.0.5
cors: 2.8.5 cors: 2.8.5
cross-spawn: 7.0.6 cross-spawn: 7.0.6
eventsource: 3.0.7 eventsource: 3.0.7
eventsource-parser: 3.0.2
express: 5.1.0 express: 5.1.0
express-rate-limit: 7.5.1(express@5.1.0) express-rate-limit: 7.5.1(express@5.1.0)
pkce-challenge: 5.0.0 pkce-challenge: 5.0.0

View file

@ -1,4 +1,4 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StreamableHTTPTransport } from '@hono/mcp' import { StreamableHTTPTransport } from '@hono/mcp'
import { Hono } from 'hono' import { Hono } from 'hono'
import { z } from 'zod' import { z } from 'zod'
@ -17,19 +17,27 @@ app.use(
// Your MCP server implementation // Your MCP server implementation
const mcpServer = new McpServer({ const mcpServer = new McpServer({
name: 'my-mcp-server', name: 'vengabus-mcp-server',
version: '1.0.0', version: '1.0.0',
}) })
mcpServer.registerTool( mcpServer.registerTool(
'add', 'calculate_party_capacity',
{ {
title: 'Addition Tool', title: 'Party Bus Capacity Calculator',
description: 'Add two numbers', description: 'Calculate how many people can fit on the Vengabus',
inputSchema: { a: z.number(), b: z.number() }, inputSchema: {
busCount: z.number().describe('Number of Vengabuses'),
peoplePerBus: z.number().describe('People per bus'),
},
}, },
async ({ a, b }) => ({ async ({ busCount, peoplePerBus }) => ({
content: [{ type: 'text', text: String(a + b) }], content: [
{
type: 'text',
text: `🚌 ${busCount} Vengabuses can transport ${busCount * peoplePerBus} party people! The wheels of steel are turning! 🎉`,
},
],
}), }),
) )
@ -42,18 +50,184 @@ mcpServer.registerTool(
inputSchema: {}, inputSchema: {},
}, },
async () => { async () => {
await scheduler.wait(100 + Math.random() * 100) // Simulate some async work
await new Promise((resolve) => setTimeout(resolve, 100 + Math.random() * 100))
return { return {
content: [ content: [
{ {
type: 'text', type: 'text',
text: `Next Vengabus: imminent. Current user's route: New York to San Francisco. Route name: "Intercity Disco".`, text: `🚌 BOOM BOOM BOOM BOOM! Next Vengabus: IMMINENT! 🎉
Current Route: "Intercity Disco Express"
From: New York
To: IBIZA - THE MAGIC ISLAND! ✨
Schedule:
- Departure: When the beat drops
- Via: San Francisco (quick party stop)
- Arrival: When the sun comes up
Status: The party bus is jumping!
Passengers: Maximum capacity!
BPM: 142 and rising!
We're going to Ibiza! Back to the island! 🏝️`,
}, },
], ],
} }
}, },
) )
// Register sample resources
mcpServer.registerResource(
'config',
'config://vengabus',
{
title: 'Vengabus Fleet Configuration',
description: 'Current Vengabus fleet settings and party parameters',
mimeType: 'application/json',
},
async (uri: URL) => {
const normalizedUri = uri.href.replace(/\/$/, '')
return {
contents: [
{
uri: normalizedUri,
mimeType: 'application/json',
text: JSON.stringify(
{
version: '1.0.0',
fleet: 'intercity-disco',
features: {
bassBoost: true,
strobeEnabled: true,
maxBPM: 160,
wheelsOfSteel: 'turning',
},
routes: ['New York to Ibiza', 'Back to the Magic Island', 'Like an Intercity Disco', 'The place to be'],
},
null,
2,
),
},
],
}
},
)
mcpServer.registerResource(
'readme',
'docs://party-manual.md',
{
title: 'Vengabus Party Manual',
description: 'Official guide to the Vengabus experience',
mimeType: 'text/markdown',
},
() => ({
contents: [
{
uri: 'docs://party-manual.md',
mimeType: 'text/markdown',
text: "# 🚌 Vengabus MCP Server\n\nBOOM BOOM BOOM BOOM! Welcome aboard the Vengabus! We like to party!\n\n## Features\n- 🎉 Party capacity calculator - How many can we take to Ibiza?\n- 🕐 Vengabus schedule checker - Next stop: The Magic Island!\n- 🎵 Fleet configuration with maximum bass boost\n- 🌟 Party statistics tracker (BPM, passengers, energy levels)\n\n## Routes\n- New York to Ibiza (via San Francisco)\n- Back to the Magic Island\n- Like an Intercity Disco\n- The place to be\n\n## Current Status\nThe wheels of steel are turning, and traffic lights are burning!\nWe're going to Ibiza! Woah! We're going to Ibiza!\n\n*Up, up and away we go!*",
}
]
}),
)
// Register a resource template
mcpServer.registerResource(
'stats',
new ResourceTemplate('party://stats/{metric}', { list: undefined }),
{
title: 'Vengabus Party Statistics',
description: 'Get party metrics (bpm, passengers, energy)',
mimeType: 'application/json',
},
async (uri: URL, { metric }) => {
return {
contents: [
{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify(
{
metric: metric,
value:
metric === 'bpm'
? 140 + Math.floor(Math.random() * 20)
: metric === 'passengers'
? Math.floor(Math.random() * 50) + 20
: metric === 'energy'
? Math.floor(Math.random() * 100)
: 0,
unit:
metric === 'bpm'
? 'beats per minute'
: metric === 'passengers'
? 'party people'
: metric === 'energy'
? 'party power %'
: 'unknown',
status: 'The party is jumping!',
lastUpdated: new Date().toISOString(),
},
null,
2,
),
},
],
}
},
)
// Register sample prompts
mcpServer.registerPrompt(
'party_invitation',
{
title: 'Vengabus Party Invitation',
description: 'Generate a party invitation for the Vengabus',
argsSchema: {
name: z.string().describe('Name of the party person'),
destination: z.string().optional().describe('Where the Vengabus is heading'),
},
},
({ name, destination }) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Create a party invitation for ${name} to join the Vengabus${destination ? ` heading to ${destination}` : ''}`,
},
},
],
}),
)
mcpServer.registerPrompt(
'party_announcement',
{
title: 'Party Bus Announcement',
description: 'Generate party-themed announcements for various occasions',
argsSchema: {
event: z.string().describe('Type of event (deployment, release, milestone, etc.)'),
details: z.string().optional().describe('Additional details about the event'),
},
},
({ event, details }) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Create a party bus announcement for a ${event}${details ? `: ${details}` : ''}`,
},
},
],
}),
)
app.post('/mcp', async (c) => { app.post('/mcp', async (c) => {
const transport = new StreamableHTTPTransport() const transport = new StreamableHTTPTransport()
await mcpServer.connect(transport) await mcpServer.connect(transport)

View file

@ -23,7 +23,7 @@ The useMcp hook manages the connection to an MCP server, handles authentication
```tsx ```tsx
import { useMcp } from 'use-mcp' import { useMcp } from 'use-mcp'
// Optional: Import types if needed // Optional: Import types if needed
import type { UseMcpOptions, UseMcpResult, Tool } from 'use-mcp' import type { UseMcpOptions, UseMcpResult, Tool, Resource, ResourceTemplate, Prompt } from 'use-mcp'
``` ```
## Usage ## Usage
@ -101,7 +101,12 @@ function MyChatComponent() {
</div> </div>
)} )}
{mcp.state === 'authenticating' && <p>Waiting for authentication...</p>} {mcp.state === 'authenticating' && <p>Waiting for authentication...</p>}
{mcp.state === 'ready' && <p>Connected! Tools available: {mcp.tools.length}</p>} {mcp.state === 'ready' && (
<p>
Connected! Tools: {mcp.tools.length}, Resources: {mcp.resources.length + mcp.resourceTemplates.length},
Prompts: {mcp.prompts.length}
</p>
)}
{/* Your Chat UI */} {/* Your Chat UI */}
<div> <div>
@ -140,6 +145,52 @@ function MyChatComponent() {
<button onClick={mcp.clearStorage} style={{ marginTop: '10px' }}> <button onClick={mcp.clearStorage} style={{ marginTop: '10px' }}>
Clear Stored Auth Clear Stored Auth
</button> </button>
{/* Example: Working with Resources */}
{mcp.state === 'ready' && mcp.resources.length > 0 && (
<div>
<h3>Available Resources:</h3>
{mcp.resources.map((resource) => (
<div key={resource.uri}>
<span>{resource.name}</span>
<button onClick={async () => {
try {
const content = await mcp.readResource(resource.uri)
console.log('Resource content:', content)
} catch (error) {
console.error('Failed to read resource:', error)
}
}}>
Read
</button>
</div>
))}
</div>
)}
{/* Example: Working with Prompts */}
{mcp.state === 'ready' && mcp.prompts.length > 0 && (
<div>
<h3>Available Prompts:</h3>
{mcp.prompts.map((prompt) => (
<div key={prompt.name}>
<span>{prompt.name}</span>
<button onClick={async () => {
try {
const result = await mcp.getPrompt(prompt.name, {
// Add any required arguments here
})
console.log('Prompt result:', result)
} catch (error) {
console.error('Failed to get prompt:', error)
}
}}>
Get Prompt
</button>
</div>
))}
</div>
)}
</div> </div>
) )
} }
@ -163,11 +214,18 @@ export default MyChatComponent
### Hook Return Value (UseMcpResult) ### Hook Return Value (UseMcpResult)
- tools: An array of Tool objects provided by the server. Empty until state is 'ready'. - tools: An array of Tool objects provided by the server. Empty until state is 'ready'.
- resources: An array of Resource objects representing data available from the server.
- resourceTemplates: An array of ResourceTemplate objects representing dynamic resources available from the server.
- prompts: An array of Prompt objects representing reusable prompt templates from the server.
- state: The current connection state ('discovering', 'authenticating', 'connecting', 'loading', 'ready', 'failed'). Use this to conditionally render UI or enable/disable features. - state: The current connection state ('discovering', 'authenticating', 'connecting', 'loading', 'ready', 'failed'). Use this to conditionally render UI or enable/disable features.
- error: An error message if state is 'failed'. - error: An error message if state is 'failed'.
- authUrl: If authentication is required and the popup was potentially blocked, this URL string can be used to let the user manually open the auth page (e.g., <a href={authUrl} target="_blank">...</a>). - authUrl: If authentication is required and the popup was potentially blocked, this URL string can be used to let the user manually open the auth page (e.g., <a href={authUrl} target="_blank">...</a>).
- log: An array of log messages { level, message, timestamp }. Useful for debugging when debug option is true. - log: An array of log messages { level, message, timestamp }. Useful for debugging when debug option is true.
- callTool(name, args): An async function to execute a tool on the MCP server. Throws an error if the client isn't ready or the call fails. - callTool(name, args): An async function to execute a tool on the MCP server. Throws an error if the client isn't ready or the call fails.
- listResources(): An async function to refresh the list of available resources. Returns void.
- readResource(uri): An async function to read the contents of a specific resource. Returns an object with a 'contents' array containing the resource data.
- listPrompts(): An async function to refresh the list of available prompts. Returns void.
- getPrompt(name, args): An async function to get a specific prompt with optional arguments. Returns an object with a 'messages' array containing the prompt messages.
- retry(): Manually triggers a reconnection attempt if the state is 'failed'. - retry(): Manually triggers a reconnection attempt if the state is 'failed'.
- disconnect(): Disconnects the client from the server. - disconnect(): Disconnects the client from the server.
- authenticate(): Manually attempts to start the authentication flow. Useful for triggering the popup via a user click if it was initially blocked. - authenticate(): Manually attempts to start the authentication flow. Useful for triggering the popup via a user click if it was initially blocked.

View file

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

View file

@ -1,4 +1,4 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js' import { Tool, Resource, ResourceTemplate, Prompt } from '@modelcontextprotocol/sdk/types.js'
export type UseMcpOptions = { export type UseMcpOptions = {
/** The /sse URL of your remote MCP server */ /** The /sse URL of your remote MCP server */
@ -35,6 +35,12 @@ export type UseMcpOptions = {
export type UseMcpResult = { export type UseMcpResult = {
/** List of tools available from the connected MCP server */ /** List of tools available from the connected MCP server */
tools: Tool[] tools: Tool[]
/** List of resources available from the connected MCP server */
resources: Resource[]
/** List of resource templates available from the connected MCP server */
resourceTemplates: ResourceTemplate[]
/** List of prompts available from the connected MCP server */
prompts: Prompt[]
/** /**
* The current state of the MCP connection: * The current state of the MCP connection:
* - 'discovering': Checking server existence and capabilities (including auth requirements). * - 'discovering': Checking server existence and capabilities (including auth requirements).
@ -63,6 +69,36 @@ export type UseMcpResult = {
* @throws If the client is not in the 'ready' state or the call fails. * @throws If the client is not in the 'ready' state or the call fails.
*/ */
callTool: (name: string, args?: Record<string, unknown>) => Promise<any> callTool: (name: string, args?: Record<string, unknown>) => Promise<any>
/**
* Function to list resources from the MCP server.
* @returns A promise that resolves when resources are refreshed.
* @throws If the client is not in the 'ready' state.
*/
listResources: () => Promise<void>
/**
* Function to read a resource from the MCP server.
* @param uri The URI of the resource to read.
* @returns A promise that resolves with the resource contents.
* @throws If the client is not in the 'ready' state or the read fails.
*/
readResource: (uri: string) => Promise<{ contents: Array<{ uri: string; mimeType?: string; text?: string; blob?: string }> }>
/**
* Function to list prompts from the MCP server.
* @returns A promise that resolves when prompts are refreshed.
* @throws If the client is not in the 'ready' state.
*/
listPrompts: () => Promise<void>
/**
* Function to get a specific prompt from the MCP server.
* @param name The name of the prompt to get.
* @param args Optional arguments for the prompt.
* @returns A promise that resolves with the prompt messages.
* @throws If the client is not in the 'ready' state or the get fails.
*/
getPrompt: (
name: string,
args?: Record<string, string>,
) => Promise<{ messages: Array<{ role: 'user' | 'assistant'; content: { type: string; text?: string; [key: string]: any } }> }>
/** Manually attempts to reconnect if the state is 'failed'. */ /** Manually attempts to reconnect if the state is 'failed'. */
retry: () => void retry: () => void
/** Disconnects the client from the MCP server. */ /** Disconnects the client from the MCP server. */

View file

@ -1,5 +1,17 @@
// useMcp.ts // useMcp.ts
import { CallToolResultSchema, JSONRPCMessage, ListToolsResultSchema, Tool } from '@modelcontextprotocol/sdk/types.js' import {
CallToolResultSchema,
JSONRPCMessage,
ListToolsResultSchema,
ListResourcesResultSchema,
ReadResourceResultSchema,
ListPromptsResultSchema,
GetPromptResultSchema,
Tool,
Resource,
ResourceTemplate,
Prompt,
} from '@modelcontextprotocol/sdk/types.js'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
// Import both transport types // Import both transport types
import { SSEClientTransport, SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js' import { SSEClientTransport, SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js'
@ -39,6 +51,9 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
const [state, setState] = useState<UseMcpResult['state']>('discovering') const [state, setState] = useState<UseMcpResult['state']>('discovering')
const [tools, setTools] = useState<Tool[]>([]) const [tools, setTools] = useState<Tool[]>([])
const [resources, setResources] = useState<Resource[]>([])
const [resourceTemplates, setResourceTemplates] = useState<ResourceTemplate[]>([])
const [prompts, setPrompts] = useState<Prompt[]>([])
const [error, setError] = useState<string | undefined>(undefined) const [error, setError] = useState<string | undefined>(undefined)
const [log, setLog] = useState<UseMcpResult['log']>([]) const [log, setLog] = useState<UseMcpResult['log']>([])
const [authUrl, setAuthUrl] = useState<string | undefined>(undefined) const [authUrl, setAuthUrl] = useState<string | undefined>(undefined)
@ -95,6 +110,9 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
if (isMountedRef.current && !quiet) { if (isMountedRef.current && !quiet) {
setState('discovering') setState('discovering')
setTools([]) setTools([])
setResources([])
setResourceTemplates([])
setPrompts([])
setError(undefined) setError(undefined)
setAuthUrl(undefined) setAuthUrl(undefined)
} }
@ -283,16 +301,49 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
await clientRef.current!.connect(transportInstance) await clientRef.current!.connect(transportInstance)
// --- Success Path --- // --- Success Path ---
addLog('info', `Client connected via ${transportType.toUpperCase()}. Loading tools...`) addLog('info', `Client connected via ${transportType.toUpperCase()}. Loading tools, resources, and prompts...`)
successfulTransportRef.current = transportType // Store successful type successfulTransportRef.current = transportType // Store successful type
setState('loading') setState('loading')
const toolsResponse = await clientRef.current!.request({ method: 'tools/list' }, ListToolsResultSchema) const toolsResponse = await clientRef.current!.request({ method: 'tools/list' }, ListToolsResultSchema)
// Load resources after tools (optional - not all servers support resources)
let resourcesResponse: { resources: Resource[]; resourceTemplates?: ResourceTemplate[] } = { resources: [], resourceTemplates: [] }
try {
resourcesResponse = await clientRef.current!.request({ method: 'resources/list' }, ListResourcesResultSchema)
} catch (err) {
addLog('debug', 'Server does not support resources/list method', err)
}
// Load prompts after resources (optional - not all servers support prompts)
let promptsResponse: { prompts: Prompt[] } = { prompts: [] }
try {
promptsResponse = await clientRef.current!.request({ method: 'prompts/list' }, ListPromptsResultSchema)
} catch (err) {
addLog('debug', 'Server does not support prompts/list method', err)
}
if (isMountedRef.current) { if (isMountedRef.current) {
// Check mount before final state updates // Check mount before final state updates
setTools(toolsResponse.tools) setTools(toolsResponse.tools)
addLog('info', `Loaded ${toolsResponse.tools.length} tools.`) setResources(resourcesResponse.resources)
setResourceTemplates(Array.isArray(resourcesResponse.resourceTemplates) ? resourcesResponse.resourceTemplates : [])
setPrompts(promptsResponse.prompts)
const summary = [`Loaded ${toolsResponse.tools.length} tools`]
if (
resourcesResponse.resources.length > 0 ||
(resourcesResponse.resourceTemplates && resourcesResponse.resourceTemplates.length > 0)
) {
summary.push(`${resourcesResponse.resources.length} resources`)
if (Array.isArray(resourcesResponse.resourceTemplates) && resourcesResponse.resourceTemplates.length > 0) {
summary.push(`${resourcesResponse.resourceTemplates.length} resource templates`)
}
}
if (promptsResponse.prompts.length > 0) {
summary.push(`${promptsResponse.prompts.length} prompts`)
}
addLog('info', summary.join(', ') + '.')
setState('ready') // Final success state setState('ready') // Final success state
// connectingRef will be set to false after orchestration logic // connectingRef will be set to false after orchestration logic
connectAttemptRef.current = 0 // Reset on success connectAttemptRef.current = 0 // Reset on success
@ -604,6 +655,88 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
} }
}, [url, addLog, disconnect]) // Depends on url and stable callbacks }, [url, addLog, disconnect]) // Depends on url and stable callbacks
// listResources is stable (depends on stable addLog)
const listResources = useCallback(async () => {
// Use stateRef for check, state for throwing error message
if (stateRef.current !== 'ready' || !clientRef.current) {
throw new Error(`MCP client is not ready (current state: ${state}). Cannot list resources.`)
}
addLog('info', 'Listing resources...')
try {
const resourcesResponse = await clientRef.current.request({ method: 'resources/list' }, ListResourcesResultSchema)
if (isMountedRef.current) {
setResources(resourcesResponse.resources)
setResourceTemplates(Array.isArray(resourcesResponse.resourceTemplates) ? resourcesResponse.resourceTemplates : [])
addLog(
'info',
`Listed ${resourcesResponse.resources.length} resources, ${Array.isArray(resourcesResponse.resourceTemplates) ? resourcesResponse.resourceTemplates.length : 0} resource templates.`,
)
}
} catch (err) {
addLog('error', `Error listing resources: ${err instanceof Error ? err.message : String(err)}`, err)
throw err
}
}, [state, addLog]) // Depends on state for error message and stable addLog
// readResource is stable (depends on stable addLog)
const readResource = useCallback(
async (uri: string) => {
// Use stateRef for check, state for throwing error message
if (stateRef.current !== 'ready' || !clientRef.current) {
throw new Error(`MCP client is not ready (current state: ${state}). Cannot read resource "${uri}".`)
}
addLog('info', `Reading resource: ${uri}`)
try {
const result = await clientRef.current.request({ method: 'resources/read', params: { uri } }, ReadResourceResultSchema)
addLog('info', `Resource "${uri}" read successfully`)
return result
} catch (err) {
addLog('error', `Error reading resource "${uri}": ${err instanceof Error ? err.message : String(err)}`, err)
throw err
}
},
[state, addLog],
) // Depends on state for error message and stable addLog
// listPrompts is stable (depends on stable addLog)
const listPrompts = useCallback(async () => {
// Use stateRef for check, state for throwing error message
if (stateRef.current !== 'ready' || !clientRef.current) {
throw new Error(`MCP client is not ready (current state: ${state}). Cannot list prompts.`)
}
addLog('info', 'Listing prompts...')
try {
const promptsResponse = await clientRef.current.request({ method: 'prompts/list' }, ListPromptsResultSchema)
if (isMountedRef.current) {
setPrompts(promptsResponse.prompts)
addLog('info', `Listed ${promptsResponse.prompts.length} prompts.`)
}
} catch (err) {
addLog('error', `Error listing prompts: ${err instanceof Error ? err.message : String(err)}`, err)
throw err
}
}, [state, addLog]) // Depends on state for error message and stable addLog
// getPrompt is stable (depends on stable addLog)
const getPrompt = useCallback(
async (name: string, args?: Record<string, string>) => {
// Use stateRef for check, state for throwing error message
if (stateRef.current !== 'ready' || !clientRef.current) {
throw new Error(`MCP client is not ready (current state: ${state}). Cannot get prompt "${name}".`)
}
addLog('info', `Getting prompt: ${name}`, args)
try {
const result = await clientRef.current.request({ method: 'prompts/get', params: { name, arguments: args } }, GetPromptResultSchema)
addLog('info', `Prompt "${name}" retrieved successfully`)
return result
} catch (err) {
addLog('error', `Error getting prompt "${name}": ${err instanceof Error ? err.message : String(err)}`, err)
throw err
}
},
[state, addLog],
) // Depends on state for error message and stable addLog
// ===== Effects ===== // ===== Effects =====
// Effect for handling auth callback messages from popup (Stable dependencies) // Effect for handling auth callback messages from popup (Stable dependencies)
@ -691,10 +824,17 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
return { return {
state, state,
tools, tools,
resources,
resourceTemplates,
prompts,
error, error,
log, log,
authUrl, authUrl,
callTool, callTool,
listResources,
readResource,
listPrompts,
getPrompt,
retry, retry,
disconnect, disconnect,
authenticate, authenticate,

View file

@ -1,5 +1,5 @@
import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest' import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'
import { chromium, Browser, Page } from 'playwright' import { chromium, type Browser, type Page } from 'playwright'
import { SERVER_CONFIGS } from './server-configs.js' import { SERVER_CONFIGS } from './server-configs.js'
import { getTestState, connectToMCPServer, cleanupProcess } from './test-utils.js' import { getTestState, connectToMCPServer, cleanupProcess } from './test-utils.js'
@ -82,9 +82,33 @@ describe('MCP Connection Integration Tests', () => {
console.log(` ${index + 1}. ${tool}`) console.log(` ${index + 1}. ${tool}`)
}) })
if (result.resources.length > 0) {
console.log(`📂 Available resources (${result.resources.length}):`)
result.resources.forEach((resource, index) => {
console.log(` ${index + 1}. ${resource}`)
})
}
if (result.prompts.length > 0) {
console.log(`💬 Available prompts (${result.prompts.length}):`)
result.prompts.forEach((prompt, index) => {
console.log(` ${index + 1}. ${prompt}`)
})
}
// Verify connection success // Verify connection success
expect(result.success).toBe(true) expect(result.success).toBe(true)
expect(result.tools.length).toBeGreaterThanOrEqual(serverConfig.expectedTools) expect(result.tools.length).toBeGreaterThanOrEqual(serverConfig.expectedTools)
// Verify resources if expected
if (serverConfig.expectedResources !== undefined) {
expect(result.resources.length).toBeGreaterThanOrEqual(serverConfig.expectedResources)
}
// Verify prompts if expected
if (serverConfig.expectedPrompts !== undefined) {
expect(result.prompts.length).toBeGreaterThanOrEqual(serverConfig.expectedPrompts)
}
} else { } else {
console.log(`❌ Failed to connect to ${serverConfig.name}`) console.log(`❌ Failed to connect to ${serverConfig.name}`)
if (result.debugLog) { if (result.debugLog) {

View file

@ -1,6 +1,6 @@
import { join, dirname } from 'path' import { join, dirname } from 'node:path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'node:url'
import { ServerConfig } from './test-utils.js' import type { ServerConfig } from './test-utils.js'
const __dirname = dirname(fileURLToPath(import.meta.url)) const __dirname = dirname(fileURLToPath(import.meta.url))
const rootDir = join(__dirname, '../..') const rootDir = join(__dirname, '../..')
@ -19,7 +19,9 @@ export const SERVER_CONFIGS: ServerConfig[] = [
transportTypes: ['auto', 'http'], transportTypes: ['auto', 'http'],
}, },
], ],
expectedTools: 1, expectedTools: 2,
expectedResources: 2, // 2 direct resources (templates are counted separately)
expectedPrompts: 2,
}, },
{ {
name: 'cf-agents', name: 'cf-agents',
@ -43,6 +45,8 @@ export const SERVER_CONFIGS: ServerConfig[] = [
transportTypes: ['auto', 'sse'], transportTypes: ['auto', 'sse'],
}, },
], ],
expectedTools: 1, expectedTools: 2,
expectedResources: 3, // 3 direct resources (no templates in cf-agents)
expectedPrompts: 2,
}, },
] ]

View file

@ -1,8 +1,8 @@
import { spawn, ChildProcess } from 'child_process' import { spawn, type ChildProcess } from 'node:child_process'
import { Page } from 'playwright' import type { Page } from 'playwright'
import { readFileSync } from 'fs' import { readFileSync } from 'node:fs'
import { join, dirname } from 'path' import { join, dirname } from 'node:path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url)) const __dirname = dirname(fileURLToPath(import.meta.url))
const testDir = join(__dirname, '..') const testDir = join(__dirname, '..')
@ -25,6 +25,8 @@ export interface ServerConfig {
portKey: keyof TestState portKey: keyof TestState
endpoints: ServerEndpoint[] endpoints: ServerEndpoint[]
expectedTools: number expectedTools: number
expectedResources?: number
expectedPrompts?: number
} }
/** /**
@ -132,7 +134,7 @@ export async function connectToMCPServer(
page: Page, page: Page,
serverUrl: string, serverUrl: string,
transportType: 'auto' | 'http' | 'sse' = 'auto', transportType: 'auto' | 'http' | 'sse' = 'auto',
): Promise<{ success: boolean; tools: string[]; debugLog: string }> { ): Promise<{ success: boolean; tools: string[]; resources: string[]; prompts: string[]; debugLog: string }> {
const state = getTestState() const state = getTestState()
if (!state.staticPort) { if (!state.staticPort) {
@ -197,7 +199,9 @@ export async function connectToMCPServer(
// Extract available tools // Extract available tools
const tools: string[] = [] const tools: string[] = []
try { try {
const toolCards = page.locator('.bg-white.rounded.border') // Look for the Available Tools section
const toolsSection = page.locator('h3:has-text("Available Tools")').locator('..')
const toolCards = toolsSection.locator('.bg-white.rounded.border')
const toolCount = await toolCards.count() const toolCount = await toolCards.count()
for (let i = 0; i < toolCount; i++) { for (let i = 0; i < toolCount; i++) {
@ -211,6 +215,44 @@ export async function connectToMCPServer(
console.warn('Could not extract tools list:', e) console.warn('Could not extract tools list:', e)
} }
// Extract available resources
const resources: string[] = []
try {
// Look for the Available Resources section
const resourcesSection = page.locator('h3:has-text("Available Resources")').locator('..')
const resourceCards = resourcesSection.locator('.bg-white.rounded.border')
const resourceCount = await resourceCards.count()
for (let i = 0; i < resourceCount; i++) {
const resourceNameElement = resourceCards.nth(i).locator('h4.font-medium.text-sm')
const resourceName = await resourceNameElement.textContent()
if (resourceName?.trim()) {
resources.push(resourceName.trim())
}
}
} catch (e) {
console.warn('Could not extract resources list:', e)
}
// Extract available prompts
const prompts: string[] = []
try {
// Look for the Available Prompts section
const promptsSection = page.locator('h3:has-text("Available Prompts")').locator('..')
const promptCards = promptsSection.locator('.bg-white.rounded.border')
const promptCount = await promptCards.count()
for (let i = 0; i < promptCount; i++) {
const promptNameElement = promptCards.nth(i).locator('h4.font-medium.text-sm')
const promptName = await promptNameElement.textContent()
if (promptName?.trim()) {
prompts.push(promptName.trim())
}
}
} catch (e) {
console.warn('Could not extract prompts list:', e)
}
// Extract debug log // Extract debug log
let debugLog = '' let debugLog = ''
try { try {
@ -225,6 +267,8 @@ export async function connectToMCPServer(
return { return {
success: isConnected, success: isConnected,
tools, tools,
resources,
prompts,
debugLog, debugLog,
} }
} }