diff --git a/src/react/types.ts b/src/react/types.ts index 9ac9aba..20e926b 100644 --- a/src/react/types.ts +++ b/src/react/types.ts @@ -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) => Promise + /** + * 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 + /** + * 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. */ diff --git a/src/react/useMcp.ts b/src/react/useMcp.ts index 8645a82..6bd715d 100644 --- a/src/react/useMcp.ts +++ b/src/react/useMcp.ts @@ -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('discovering') const [tools, setTools] = useState([]) + const [resources, setResources] = useState([]) + const [resourceTemplates, setResourceTemplates] = useState([]) const [error, setError] = useState(undefined) const [log, setLog] = useState([]) const [authUrl, setAuthUrl] = useState(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,