feat: add MCP resources support to useMcp hook

- Add Resource and ResourceTemplate types from MCP SDK
- Add resources and resourceTemplates arrays to UseMcpResult
- Fetch resources automatically after tools during connection
- Add listResources() method to manually refresh resources list
- Add readResource(uri) method to read resource contents
- Clear resources state on disconnect

🤖 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:35:37 -07:00
commit 8373adfdf9
2 changed files with 89 additions and 4 deletions

View file

@ -1,4 +1,4 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js'
import { Tool, Resource, ResourceTemplate } from '@modelcontextprotocol/sdk/types.js'
export type UseMcpOptions = {
/** The /sse URL of your remote MCP server */
@ -35,6 +35,10 @@ export type UseMcpOptions = {
export type UseMcpResult = {
/** List of tools available from the connected MCP server */
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[]
/**
* The current state of the MCP connection:
* - 'discovering': Checking server existence and capabilities (including auth requirements).
@ -63,6 +67,19 @@ export type UseMcpResult = {
* @throws If the client is not in the 'ready' state or the call fails.
*/
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 }> }>
/** Manually attempts to reconnect if the state is 'failed'. */
retry: () => void
/** Disconnects the client from the MCP server. */

View file

@ -1,5 +1,14 @@
// useMcp.ts
import { CallToolResultSchema, JSONRPCMessage, ListToolsResultSchema, Tool } from '@modelcontextprotocol/sdk/types.js'
import {
CallToolResultSchema,
JSONRPCMessage,
ListToolsResultSchema,
ListResourcesResultSchema,
ReadResourceResultSchema,
Tool,
Resource,
ResourceTemplate,
} from '@modelcontextprotocol/sdk/types.js'
import { useCallback, useEffect, useRef, useState } from 'react'
// Import both transport types
import { SSEClientTransport, SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js'
@ -39,6 +48,8 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
const [state, setState] = useState<UseMcpResult['state']>('discovering')
const [tools, setTools] = useState<Tool[]>([])
const [resources, setResources] = useState<Resource[]>([])
const [resourceTemplates, setResourceTemplates] = useState<ResourceTemplate[]>([])
const [error, setError] = useState<string | undefined>(undefined)
const [log, setLog] = useState<UseMcpResult['log']>([])
const [authUrl, setAuthUrl] = useState<string | undefined>(undefined)
@ -95,6 +106,8 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
if (isMountedRef.current && !quiet) {
setState('discovering')
setTools([])
setResources([])
setResourceTemplates([])
setError(undefined)
setAuthUrl(undefined)
}
@ -283,16 +296,24 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
await clientRef.current!.connect(transportInstance)
// --- Success Path ---
addLog('info', `Client connected via ${transportType.toUpperCase()}. Loading tools...`)
addLog('info', `Client connected via ${transportType.toUpperCase()}. Loading tools and resources...`)
successfulTransportRef.current = transportType // Store successful type
setState('loading')
const toolsResponse = await clientRef.current!.request({ method: 'tools/list' }, ListToolsResultSchema)
// Load resources after tools
const resourcesResponse = await clientRef.current!.request({ method: 'resources/list' }, ListResourcesResultSchema)
if (isMountedRef.current) {
// Check mount before final state updates
setTools(toolsResponse.tools)
addLog('info', `Loaded ${toolsResponse.tools.length} tools.`)
setResources(resourcesResponse.resources)
setResourceTemplates(Array.isArray(resourcesResponse.resourceTemplates) ? resourcesResponse.resourceTemplates : [])
addLog(
'info',
`Loaded ${toolsResponse.tools.length} tools, ${resourcesResponse.resources.length} resources, ${Array.isArray(resourcesResponse.resourceTemplates) ? resourcesResponse.resourceTemplates.length : 0} resource templates.`,
)
setState('ready') // Final success state
// connectingRef will be set to false after orchestration logic
connectAttemptRef.current = 0 // Reset on success
@ -604,6 +625,49 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
}
}, [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
// ===== Effects =====
// Effect for handling auth callback messages from popup (Stable dependencies)
@ -691,10 +755,14 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
return {
state,
tools,
resources,
resourceTemplates,
error,
log,
authUrl,
callTool,
listResources,
readResource,
retry,
disconnect,
authenticate,