From 8373adfdf925a1b393ab109f04a24f65d258af11 Mon Sep 17 00:00:00 2001 From: Jem <591643+jem-computer@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:35:37 -0700 Subject: [PATCH 1/7] feat: add MCP resources support to useMcp hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/react/types.ts | 19 +++++++++++- src/react/useMcp.ts | 74 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 89 insertions(+), 4 deletions(-) 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, From 8377abf268c22be566af1ce95d6a477f851290bf Mon Sep 17 00:00:00 2001 From: Jem <591643+jem-computer@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:41:11 -0700 Subject: [PATCH 2/7] feat: add MCP prompts support to useMcp hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Prompt type from MCP SDK - Add prompts array to UseMcpResult - Fetch prompts automatically after resources during connection - Add listPrompts() method to manually refresh prompts list - Add getPrompt(name, args) method to get specific prompt messages - Clear prompts state on disconnect 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/react/types.ts | 21 ++++++++++++++++- src/react/useMcp.ts | 55 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/react/types.ts b/src/react/types.ts index 20e926b..61934ea 100644 --- a/src/react/types.ts +++ b/src/react/types.ts @@ -1,4 +1,4 @@ -import { Tool, Resource, ResourceTemplate } from '@modelcontextprotocol/sdk/types.js' +import { Tool, Resource, ResourceTemplate, Prompt } from '@modelcontextprotocol/sdk/types.js' export type UseMcpOptions = { /** The /sse URL of your remote MCP server */ @@ -39,6 +39,8 @@ export type UseMcpResult = { 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: * - 'discovering': Checking server existence and capabilities (including auth requirements). @@ -80,6 +82,23 @@ export type UseMcpResult = { * @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 + /** + * 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, + ) => Promise<{ messages: Array<{ role: 'user' | 'assistant'; content: { type: string; text?: string; [key: string]: any } }> }> /** 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 6bd715d..fa5a18a 100644 --- a/src/react/useMcp.ts +++ b/src/react/useMcp.ts @@ -5,9 +5,12 @@ import { ListToolsResultSchema, ListResourcesResultSchema, ReadResourceResultSchema, + ListPromptsResultSchema, + GetPromptResultSchema, Tool, Resource, ResourceTemplate, + Prompt, } from '@modelcontextprotocol/sdk/types.js' import { useCallback, useEffect, useRef, useState } from 'react' // Import both transport types @@ -50,6 +53,7 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { const [tools, setTools] = useState([]) const [resources, setResources] = useState([]) const [resourceTemplates, setResourceTemplates] = useState([]) + const [prompts, setPrompts] = useState([]) const [error, setError] = useState(undefined) const [log, setLog] = useState([]) const [authUrl, setAuthUrl] = useState(undefined) @@ -108,6 +112,7 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { setTools([]) setResources([]) setResourceTemplates([]) + setPrompts([]) setError(undefined) setAuthUrl(undefined) } @@ -296,7 +301,7 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { await clientRef.current!.connect(transportInstance) // --- Success Path --- - addLog('info', `Client connected via ${transportType.toUpperCase()}. Loading tools and resources...`) + addLog('info', `Client connected via ${transportType.toUpperCase()}. Loading tools, resources, and prompts...`) successfulTransportRef.current = transportType // Store successful type setState('loading') @@ -305,14 +310,18 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { // Load resources after tools const resourcesResponse = await clientRef.current!.request({ method: 'resources/list' }, ListResourcesResultSchema) + // Load prompts after resources + const promptsResponse = await clientRef.current!.request({ method: 'prompts/list' }, ListPromptsResultSchema) + if (isMountedRef.current) { // Check mount before final state updates setTools(toolsResponse.tools) setResources(resourcesResponse.resources) setResourceTemplates(Array.isArray(resourcesResponse.resourceTemplates) ? resourcesResponse.resourceTemplates : []) + setPrompts(promptsResponse.prompts) addLog( 'info', - `Loaded ${toolsResponse.tools.length} tools, ${resourcesResponse.resources.length} resources, ${Array.isArray(resourcesResponse.resourceTemplates) ? resourcesResponse.resourceTemplates.length : 0} resource templates.`, + `Loaded ${toolsResponse.tools.length} tools, ${resourcesResponse.resources.length} resources, ${Array.isArray(resourcesResponse.resourceTemplates) ? resourcesResponse.resourceTemplates.length : 0} resource templates, ${promptsResponse.prompts.length} prompts.`, ) setState('ready') // Final success state // connectingRef will be set to false after orchestration logic @@ -668,6 +677,45 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { [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) => { + // 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 ===== // Effect for handling auth callback messages from popup (Stable dependencies) @@ -757,12 +805,15 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { tools, resources, resourceTemplates, + prompts, error, log, authUrl, callTool, listResources, readResource, + listPrompts, + getPrompt, retry, disconnect, authenticate, From 7a923c350956f928af4110857944145ccc63540b Mon Sep 17 00:00:00 2001 From: Jem <591643+jem-computer@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:46:06 -0700 Subject: [PATCH 3/7] feat(examples): add resources and prompts display to inspector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../inspector/src/components/McpServers.tsx | 273 +++++++++++++++++- src/react/index.ts | 4 +- 2 files changed, 269 insertions(+), 8 deletions(-) diff --git a/examples/inspector/src/components/McpServers.tsx b/examples/inspector/src/components/McpServers.tsx index b99160d..95b52fc 100644 --- a/examples/inspector/src/components/McpServers.tsx +++ b/examples/inspector/src/components/McpServers.tsx @@ -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({ state: 'not-connected', tools: [], + resources: [], + resourceTemplates: [], + prompts: [], error: undefined, log: [], authUrl: undefined, retry: () => {}, disconnect: () => {}, authenticate: () => Promise.resolve(undefined), - callTool: (_name: string, _args?: Record) => Promise.resolve(undefined), + callTool: (_name: string, _args?: Record) => + Promise.resolve(undefined), + listResources: () => Promise.resolve(), + readResource: (_uri: string) => Promise.resolve({ contents: [] }), + listPrompts: () => Promise.resolve(), + getPrompt: (_name: string, _args?: Record) => + Promise.resolve({ messages: [] }), clearStorage: () => {}, }) const [toolForms, setToolForms] = useState>>({}) const [toolExecutionLogs, setToolExecutionLogs] = useState>({}) const [expandedTools, setExpandedTools] = useState>({}) + const [expandedResources, setExpandedResources] = useState>({}) + const [expandedPrompts, setExpandedPrompts] = useState>({}) + const [resourceContents, setResourceContents] = useState>({}) + const [promptResults, setPromptResults] = useState>({}) + const [promptArgs, setPromptArgs] = useState>>({}) const logRef = useRef(null) const executionLogRefs = useRef>({}) // 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) => Promise.resolve(undefined), + callTool: (_name: string, _args?: Record) => + Promise.resolve(undefined), + listResources: () => Promise.resolve(), + readResource: (_uri: string) => Promise.resolve({ contents: [] }), + listPrompts: () => Promise.resolve(), + getPrompt: (_name: string, _args?: Record) => + Promise.resolve({ messages: [] }), clearStorage: () => {}, }) } @@ -460,6 +493,234 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[]) )} + {/* Available Resources section */} +
+

+ Available Resources ({resources.length + resourceTemplates.length}) +

+ + {resources.length === 0 && resourceTemplates.length === 0 ? ( +
+ No resources available. Connect to an MCP server to see available resources. +
+ ) : ( +
+ {/* Direct Resources */} + {resources.map((resource: Resource, index: number) => { + const isExpanded = expandedResources[resource.uri] || false + const contents = resourceContents[resource.uri] + + return ( +
+
+
+ +
+
+ +

{resource.name}

+
+ {resource.description && ( +

{resource.description}

+ )} +

{resource.uri}

+ {resource.mimeType && ( + ({resource.mimeType}) + )} +
+
+ + {isExpanded && ( +
+ + + {contents && ( +
+ {contents.error ? ( +
+ Error: {contents.error} +
+ ) : ( +
+ {contents.contents?.map((content: any, idx: number) => ( +
+
+ {content.uri} {content.mimeType && `(${content.mimeType})`} +
+
+                                        {content.text || (content.blob && '[Binary content]') || '[No content]'}
+                                      
+
+ ))} +
+ )} +
+ )} +
+ )} +
+
+ ) + })} + + {/* Resource Templates */} + {resourceTemplates.map((template: ResourceTemplate, index: number) => ( +
+
+
+ +

{template.name}

+ Template +
+ {template.description && ( +

{template.description}

+ )} +

{template.uriTemplate}

+ {template.mimeType && ( + ({template.mimeType}) + )} +
+
+ ))} +
+ )} +
+ + {/* Available Prompts section */} +
+

+ Available Prompts ({prompts.length}) +

+ + {prompts.length === 0 ? ( +
+ No prompts available. Connect to an MCP server to see available prompts. +
+ ) : ( +
+ {prompts.map((prompt: Prompt, index: number) => { + const isExpanded = expandedPrompts[prompt.name] || false + const result = promptResults[prompt.name] + const args = promptArgs[prompt.name] || {} + + return ( +
+
+
+ +
+
+ +

{prompt.name}

+
+ {prompt.description && ( +

{prompt.description}

+ )} +
+
+ + {isExpanded && ( +
+ {/* Prompt Arguments */} + {prompt.arguments && prompt.arguments.length > 0 && ( +
+
Arguments:
+ {prompt.arguments.map((arg: any) => ( +
+ + setPromptArgs(prev => ({ + ...prev, + [prompt.name]: { + ...prev[prompt.name], + [arg.name]: e.target.value + } + }))} + placeholder={arg.description || `Enter ${arg.name}`} + /> +
+ ))} +
+ )} + + + + {result && ( +
+ {result.error ? ( +
+ Error: {result.error} +
+ ) : ( +
+
Messages:
+ {result.messages?.map((message: any, idx: number) => ( +
+
+ {message.role}: +
+
+                                        {message.content.text || JSON.stringify(message.content, null, 2)}
+                                      
+
+ ))} +
+ )} +
+ )} +
+ )} +
+
+ ) + })} +
+ )} +
+ {/* Debug Log */}
diff --git a/src/react/index.ts b/src/react/index.ts index c2aedd7..25c9f76 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -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' From d4220ec5f2a1fb23705ada26477f24b278d7a1f9 Mon Sep 17 00:00:00 2001 From: Jem <591643+jem-computer@users.noreply.github.com> Date: Fri, 20 Jun 2025 01:15:59 -0700 Subject: [PATCH 4/7] fix: make resources and prompts optional features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wrap resources/list and prompts/list calls in try-catch blocks - Continue with connection even if server doesn't support these methods - Update logging to only show counts for features that are supported - Fix TypeScript types for optional responses This allows the hook to work with servers that only implement the core tools functionality without resources or prompts support. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/react/useMcp.ts | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/react/useMcp.ts b/src/react/useMcp.ts index fa5a18a..f95b4f2 100644 --- a/src/react/useMcp.ts +++ b/src/react/useMcp.ts @@ -307,11 +307,21 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { const toolsResponse = await clientRef.current!.request({ method: 'tools/list' }, ListToolsResultSchema) - // Load resources after tools - const resourcesResponse = await clientRef.current!.request({ method: 'resources/list' }, ListResourcesResultSchema) + // 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 - const promptsResponse = await clientRef.current!.request({ method: 'prompts/list' }, ListPromptsResultSchema) + // 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) { // Check mount before final state updates @@ -319,10 +329,21 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { setResources(resourcesResponse.resources) setResourceTemplates(Array.isArray(resourcesResponse.resourceTemplates) ? resourcesResponse.resourceTemplates : []) setPrompts(promptsResponse.prompts) - addLog( - 'info', - `Loaded ${toolsResponse.tools.length} tools, ${resourcesResponse.resources.length} resources, ${Array.isArray(resourcesResponse.resourceTemplates) ? resourcesResponse.resourceTemplates.length : 0} resource templates, ${promptsResponse.prompts.length} 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 // connectingRef will be set to false after orchestration logic connectAttemptRef.current = 0 // Reset on success From b909730ae29fd780c3da714031bc781f375ef697 Mon Sep 17 00:00:00 2001 From: Jem <591643+jem-computer@users.noreply.github.com> Date: Fri, 20 Jun 2025 01:39:09 -0700 Subject: [PATCH 5/7] docs: update README files with resources and prompts documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add resources, resourceTemplates, and prompts to all feature lists - Document new properties returned by useMcp hook - Document new methods: listResources, readResource, listPrompts, getPrompt - Add code examples showing how to use resources and prompts - Update quick start examples to demonstrate all features - Add note about graceful handling of servers with partial MCP support 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 40 ++++++++ examples/inspector/README.md | 18 +++- .../inspector/src/components/McpServers.tsx | 96 ++++++++----------- src/react/README.md | 62 +++++++++++- 4 files changed, 153 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 704831c..5e69e96 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,9 @@ yarn add use-mcp - 🔄 Automatic connection management with reconnection and retries - 🔐 OAuth authentication flow handling with popup and fallback support - 📦 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 - 📝 Comprehensive logging for debugging - 🌐 Works with both HTTP and SSE (Server-Sent Events) transports @@ -38,8 +41,12 @@ function MyAIComponent() { const { state, // Connection state: 'discovering' | 'pending_auth' | 'authenticating' | 'connecting' | 'loading' | 'ready' | 'failed' tools, // Available tools from MCP server + resources, // Available resources from MCP server + prompts, // Available prompts from MCP server error, // Error message if connection failed 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 authenticate, // Manually trigger authentication clearStorage, // Clear stored tokens and credentials @@ -83,6 +90,32 @@ function MyAIComponent() { ))} + + {/* Example: Display and read resources */} + {resources.length > 0 && ( +
+

Resources: {resources.length}

+ +
+ )} + + {/* Example: Use prompts */} + {prompts.length > 0 && ( +
+

Prompts: {prompts.length}

+ +
+ )}
) } @@ -176,10 +209,17 @@ function useMcp(options: UseMcpOptions): UseMcpResult |----------|------|-------------| | `state` | `string` | Current connection state: 'discovering', 'pending_auth', 'authenticating', 'connecting', 'loading', 'ready', 'failed' | | `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 | | `authUrl` | `string \| undefined` | Manual authentication URL if popup is blocked | | `log` | `{ level: 'debug' \| 'info' \| 'warn' \| 'error'; message: string; timestamp: number }[]` | Array of log messages | | `callTool` | `(name: string, args?: Record) => Promise` | Function to call a tool on the MCP server | +| `listResources` | `() => Promise` | Refresh the list of available resources | +| `readResource` | `(uri: string) => Promise<{ contents: Array<...> }>` | Read the contents of a specific resource | +| `listPrompts` | `() => Promise` | Refresh the list of available prompts | +| `getPrompt` | `(name: string, args?: Record) => Promise<{ messages: Array<...> }>` | Get a specific prompt with optional arguments | | `retry` | `() => void` | Manually attempt to reconnect | | `disconnect` | `() => void` | Disconnect from the MCP server | | `authenticate` | `() => void` | Manually trigger authentication | diff --git a/examples/inspector/README.md b/examples/inspector/README.md index d137ed3..b29c779 100644 --- a/examples/inspector/README.md +++ b/examples/inspector/README.md @@ -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 - View available tools and their schemas +- Browse and read server resources +- Interact with server-provided prompts - Real-time connection status monitoring - Debug logging for troubleshooting - Clean, minimal UI focused on MCP functionality @@ -31,7 +33,7 @@ pnpm dev 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 @@ -46,7 +48,17 @@ const connection = useMcp({ 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. diff --git a/examples/inspector/src/components/McpServers.tsx b/examples/inspector/src/components/McpServers.tsx index 95b52fc..7f397c6 100644 --- a/examples/inspector/src/components/McpServers.tsx +++ b/examples/inspector/src/components/McpServers.tsx @@ -60,13 +60,11 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[]) retry: () => {}, disconnect: () => {}, authenticate: () => Promise.resolve(undefined), - callTool: (_name: string, _args?: Record) => - Promise.resolve(undefined), + callTool: (_name: string, _args?: Record) => Promise.resolve(undefined), listResources: () => Promise.resolve(), readResource: (_uri: string) => Promise.resolve({ contents: [] }), listPrompts: () => Promise.resolve(), - getPrompt: (_name: string, _args?: Record) => - Promise.resolve({ messages: [] }), + getPrompt: (_name: string, _args?: Record) => Promise.resolve({ messages: [] }), clearStorage: () => {}, }) const [toolForms, setToolForms] = useState>>({}) @@ -118,13 +116,11 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[]) retry: () => {}, disconnect: () => {}, authenticate: () => Promise.resolve(undefined), - callTool: (_name: string, _args?: Record) => - Promise.resolve(undefined), + callTool: (_name: string, _args?: Record) => Promise.resolve(undefined), listResources: () => Promise.resolve(), readResource: (_uri: string) => Promise.resolve({ contents: [] }), listPrompts: () => Promise.resolve(), - getPrompt: (_name: string, _args?: Record) => - Promise.resolve({ messages: [] }), + getPrompt: (_name: string, _args?: Record) => Promise.resolve({ messages: [] }), clearStorage: () => {}, }) } @@ -495,10 +491,8 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[]) {/* Available Resources section */}
-

- Available Resources ({resources.length + resourceTemplates.length}) -

- +

Available Resources ({resources.length + resourceTemplates.length})

+ {resources.length === 0 && resourceTemplates.length === 0 ? (
No resources available. Connect to an MCP server to see available resources. @@ -509,13 +503,13 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[]) {resources.map((resource: Resource, index: number) => { const isExpanded = expandedResources[resource.uri] || false const contents = resourceContents[resource.uri] - + return (
- {resource.description && ( -

{resource.description}

- )} + {resource.description &&

{resource.description}

}

{resource.uri}

- {resource.mimeType && ( - ({resource.mimeType}) - )} + {resource.mimeType && ({resource.mimeType})}
- + {isExpanded && (
- + {contents && (
{contents.error ? ( @@ -579,7 +569,7 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
) })} - + {/* Resource Templates */} {resourceTemplates.map((template: ResourceTemplate, index: number) => (
@@ -589,13 +579,9 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])

{template.name}

Template
- {template.description && ( -

{template.description}

- )} + {template.description &&

{template.description}

}

{template.uriTemplate}

- {template.mimeType && ( - ({template.mimeType}) - )} + {template.mimeType && ({template.mimeType})}
))} @@ -605,10 +591,8 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[]) {/* Available Prompts section */}
-

- Available Prompts ({prompts.length}) -

- +

Available Prompts ({prompts.length})

+ {prompts.length === 0 ? (
No prompts available. Connect to an MCP server to see available prompts. @@ -619,13 +603,13 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[]) const isExpanded = expandedPrompts[prompt.name] || false const result = promptResults[prompt.name] const args = promptArgs[prompt.name] || {} - + return (
- {prompt.description && ( -

{prompt.description}

- )} + {prompt.description &&

{prompt.description}

}
- + {isExpanded && (
{/* Prompt Arguments */} @@ -652,42 +634,42 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[]) setPromptArgs(prev => ({ - ...prev, - [prompt.name]: { - ...prev[prompt.name], - [arg.name]: e.target.value - } - }))} + onChange={(e) => + setPromptArgs((prev) => ({ + ...prev, + [prompt.name]: { + ...prev[prompt.name], + [arg.name]: e.target.value, + }, + })) + } placeholder={arg.description || `Enter ${arg.name}`} />
))}
)} - + - + {result && (
{result.error ? ( @@ -699,9 +681,7 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
Messages:
{result.messages?.map((message: any, idx: number) => (
-
- {message.role}: -
+
{message.role}:
                                         {message.content.text || JSON.stringify(message.content, null, 2)}
                                       
diff --git a/src/react/README.md b/src/react/README.md index 3e15c47..f523ea5 100644 --- a/src/react/README.md +++ b/src/react/README.md @@ -23,7 +23,7 @@ The useMcp hook manages the connection to an MCP server, handles authentication ```tsx import { useMcp } from 'use-mcp' // 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 @@ -101,7 +101,12 @@ function MyChatComponent() {
)} {mcp.state === 'authenticating' &&

Waiting for authentication...

} - {mcp.state === 'ready' &&

Connected! Tools available: {mcp.tools.length}

} + {mcp.state === 'ready' && ( +

+ Connected! Tools: {mcp.tools.length}, Resources: {mcp.resources.length + mcp.resourceTemplates.length}, + Prompts: {mcp.prompts.length} +

+ )} {/* Your Chat UI */}
@@ -140,6 +145,52 @@ function MyChatComponent() { + + {/* Example: Working with Resources */} + {mcp.state === 'ready' && mcp.resources.length > 0 && ( +
+

Available Resources:

+ {mcp.resources.map((resource) => ( +
+ {resource.name} + +
+ ))} +
+ )} + + {/* Example: Working with Prompts */} + {mcp.state === 'ready' && mcp.prompts.length > 0 && ( +
+

Available Prompts:

+ {mcp.prompts.map((prompt) => ( +
+ {prompt.name} + +
+ ))} +
+ )}
) } @@ -163,11 +214,18 @@ export default MyChatComponent ### Hook Return Value (UseMcpResult) - 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. - 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., ...). - 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. +- 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'. - 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. From e0743c35e447f21e76dc2bb46d14dbe6e62bfba6 Mon Sep 17 00:00:00 2001 From: Jem <591643+jem-computer@users.noreply.github.com> Date: Wed, 2 Jul 2025 17:59:24 -0700 Subject: [PATCH 6/7] chore: fix prettier formatting and update example server dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix formatting issues in cf-agents, hono-mcp, and test files - Update dependencies in example servers 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- examples/servers/cf-agents/package.json | 2 +- examples/servers/cf-agents/pnpm-lock.yaml | 13 +- examples/servers/cf-agents/src/index.ts | 91 +++++++++++++- examples/servers/hono-mcp/package.json | 2 +- examples/servers/hono-mcp/pnpm-lock.yaml | 17 +-- examples/servers/hono-mcp/src/index.ts | 138 +++++++++++++++++++++- test/integration/mcp-connection.test.ts | 26 +++- test/integration/server-configs.ts | 14 ++- test/integration/test-utils.ts | 58 +++++++-- 9 files changed, 329 insertions(+), 32 deletions(-) diff --git a/examples/servers/cf-agents/package.json b/examples/servers/cf-agents/package.json index 180b46c..17cf03c 100644 --- a/examples/servers/cf-agents/package.json +++ b/examples/servers/cf-agents/package.json @@ -12,7 +12,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.13.0", + "@modelcontextprotocol/sdk": "^1.13.3", "zod": "^3.25.67" }, "devDependencies": { diff --git a/examples/servers/cf-agents/pnpm-lock.yaml b/examples/servers/cf-agents/pnpm-lock.yaml index 7c3d7be..010e5d1 100644 --- a/examples/servers/cf-agents/pnpm-lock.yaml +++ b/examples/servers/cf-agents/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@modelcontextprotocol/sdk': - specifier: ^1.13.0 - version: 1.13.2 + specifier: ^1.13.3 + version: 1.13.3 zod: specifier: ^3.25.67 version: 3.25.67 @@ -443,8 +443,8 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@modelcontextprotocol/sdk@1.13.2': - resolution: {integrity: sha512-Vx7qOcmoKkR3qhaQ9qf3GxiVKCEu+zfJddHv6x3dY/9P6+uIwJnmuAur5aB+4FDXf41rRrDnOEGkviX5oYZ67w==} + '@modelcontextprotocol/sdk@1.13.3': + resolution: {integrity: sha512-bGwA78F/U5G2jrnsdRkPY3IwIwZeWUEfb5o764b79lb0rJmMT76TLwKhdNZOWakOQtedYefwIR4emisEMvInKA==} engines: {node: '>=18'} '@opentelemetry/api@1.9.0': @@ -1314,13 +1314,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.2 - '@modelcontextprotocol/sdk@1.13.2': + '@modelcontextprotocol/sdk@1.13.3': dependencies: ajv: 6.12.6 content-type: 1.0.5 cors: 2.8.5 cross-spawn: 7.0.6 eventsource: 3.0.7 + eventsource-parser: 3.0.3 express: 5.1.0 express-rate-limit: 7.5.1(express@5.1.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): dependencies: - '@modelcontextprotocol/sdk': 1.13.2 + '@modelcontextprotocol/sdk': 1.13.3 ai: 4.3.16(react@19.1.0)(zod@3.25.67) cron-schedule: 5.0.4 nanoid: 5.1.5 diff --git a/examples/servers/cf-agents/src/index.ts b/examples/servers/cf-agents/src/index.ts index 417e3b9..f474d0f 100644 --- a/examples/servers/cf-agents/src/index.ts +++ b/examples/servers/cf-agents/src/index.ts @@ -1,7 +1,7 @@ import { McpAgent } from 'agents/mcp' import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' 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' // Define our MCP agent with tools @@ -53,6 +53,95 @@ export class MyMCP extends McpAgent { 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`, + }, + }, + ], + })) } } diff --git a/examples/servers/hono-mcp/package.json b/examples/servers/hono-mcp/package.json index 7e7adb3..f4acdf7 100644 --- a/examples/servers/hono-mcp/package.json +++ b/examples/servers/hono-mcp/package.json @@ -10,7 +10,7 @@ }, "devDependencies": { "@hono/mcp": "^0.1.0", - "@modelcontextprotocol/sdk": "^1.13.1", + "@modelcontextprotocol/sdk": "^1.13.3", "wrangler": "^4.21.0", "zod": "^3.25.67" } diff --git a/examples/servers/hono-mcp/pnpm-lock.yaml b/examples/servers/hono-mcp/pnpm-lock.yaml index d03e141..7e74fb7 100644 --- a/examples/servers/hono-mcp/pnpm-lock.yaml +++ b/examples/servers/hono-mcp/pnpm-lock.yaml @@ -14,10 +14,10 @@ importers: devDependencies: '@hono/mcp': 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': - specifier: ^1.13.1 - version: 1.13.1 + specifier: ^1.13.3 + version: 1.13.3 wrangler: specifier: ^4.21.0 version: 4.21.0 @@ -352,8 +352,8 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@modelcontextprotocol/sdk@1.13.1': - resolution: {integrity: sha512-8q6+9aF0yA39/qWT/uaIj6zTpC+Qu07DnN/lb9mjoquCJsAh6l3HyYqc9O3t2j7GilseOQOQimLg7W3By6jqvg==} + '@modelcontextprotocol/sdk@1.13.3': + resolution: {integrity: sha512-bGwA78F/U5G2jrnsdRkPY3IwIwZeWUEfb5o764b79lb0rJmMT76TLwKhdNZOWakOQtedYefwIR4emisEMvInKA==} engines: {node: '>=18'} accepts@2.0.0: @@ -972,9 +972,9 @@ snapshots: '@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: - '@modelcontextprotocol/sdk': 1.13.1 + '@modelcontextprotocol/sdk': 1.13.3 hono: 4.8.2 '@img/sharp-darwin-arm64@0.33.5': @@ -1061,13 +1061,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@modelcontextprotocol/sdk@1.13.1': + '@modelcontextprotocol/sdk@1.13.3': dependencies: ajv: 6.12.6 content-type: 1.0.5 cors: 2.8.5 cross-spawn: 7.0.6 eventsource: 3.0.7 + eventsource-parser: 3.0.2 express: 5.1.0 express-rate-limit: 7.5.1(express@5.1.0) pkce-challenge: 5.0.0 diff --git a/examples/servers/hono-mcp/src/index.ts b/examples/servers/hono-mcp/src/index.ts index 5fc2067..da2ba96 100644 --- a/examples/servers/hono-mcp/src/index.ts +++ b/examples/servers/hono-mcp/src/index.ts @@ -1,4 +1,4 @@ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { McpServer, type ReadResourceTemplateCallback, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js' import { StreamableHTTPTransport } from '@hono/mcp' import { Hono } from 'hono' import { z } from 'zod' @@ -42,7 +42,8 @@ mcpServer.registerTool( inputSchema: {}, }, async () => { - await scheduler.wait(100 + Math.random() * 100) + // Simulate some async work + await new Promise((resolve) => setTimeout(resolve, 100 + Math.random() * 100)) return { content: [ { @@ -54,6 +55,139 @@ mcpServer.registerTool( }, ) +// Register sample resources +mcpServer.registerResource( + 'config', + 'config://app', + { + title: 'Application Configuration', + description: 'Current application configuration settings', + mimeType: 'application/json', + }, + async (uri: URL) => ({ + contents: [ + { + uri: uri.href, + mimeType: 'application/json', + text: JSON.stringify( + { + version: '1.0.0', + environment: 'test', + features: { + vengabus: true, + calculator: true, + }, + }, + null, + 2, + ), + }, + ], + }), +) + +mcpServer.registerResource( + 'readme', + 'file://readme.md', + { + title: 'README', + description: 'Server documentation', + mimeType: 'text/markdown', + }, + async (uri: URL) => ({ + contents: [ + { + uri: uri.href, + mimeType: 'text/markdown', + text: '# Test MCP Server\n\nThis is a test server for the use-mcp integration tests.\n\n## Features\n- Addition tool\n- Vengabus schedule checker\n- Sample resources\n- Sample prompts', + }, + ], + }), +) + +// Register a resource template +mcpServer.registerResource( + 'stats', + new ResourceTemplate('data://stats/{type}', { list: undefined }), + { + title: 'Statistics Data', + description: 'Get statistics for different types', + mimeType: 'application/json', + }, + // @ts-expect-error - type is missing in the callback + async (uri: URL, { type }) => ({ + contents: [ + { + uri: uri.href, + mimeType: 'application/json', + text: JSON.stringify( + { + type: type, + count: Math.floor(Math.random() * 100), + lastUpdated: new Date().toISOString(), + }, + null, + 2, + ), + }, + ], + }), +) + +// Register sample prompts +mcpServer.registerPrompt( + 'greeting', + { + title: 'Generate Greeting', + description: 'Generate a greeting message', + argsSchema: { + name: z.string().describe('Name of the person to greet'), + style: z.string().optional().describe('Style of greeting (formal/casual)'), + }, + }, + ({ name, style }) => ({ + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Generate a ${style || 'casual'} greeting for ${name}`, + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: style === 'formal' ? `Good day, ${name}. I hope this message finds you well.` : `Hey ${name}! How's it going?`, + }, + }, + ], + }), +) + +mcpServer.registerPrompt( + 'code_review', + { + title: 'Code Review Template', + description: 'Template for code review requests', + argsSchema: { + language: z.string().describe('Programming language'), + focus: z.string().optional().describe('What to focus on (performance/security/style)'), + }, + }, + ({ language, focus }) => ({ + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Please review the following ${language} code${focus ? ` with a focus on ${focus}` : ''}:`, + }, + }, + ], + }), +) + app.post('/mcp', async (c) => { const transport = new StreamableHTTPTransport() await mcpServer.connect(transport) diff --git a/test/integration/mcp-connection.test.ts b/test/integration/mcp-connection.test.ts index a8c8a19..c60a544 100644 --- a/test/integration/mcp-connection.test.ts +++ b/test/integration/mcp-connection.test.ts @@ -1,5 +1,5 @@ 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 { getTestState, connectToMCPServer, cleanupProcess } from './test-utils.js' @@ -82,9 +82,33 @@ describe('MCP Connection Integration Tests', () => { 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 expect(result.success).toBe(true) 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 { console.log(`❌ Failed to connect to ${serverConfig.name}`) if (result.debugLog) { diff --git a/test/integration/server-configs.ts b/test/integration/server-configs.ts index 3b93389..47f1ad6 100644 --- a/test/integration/server-configs.ts +++ b/test/integration/server-configs.ts @@ -1,6 +1,6 @@ -import { join, dirname } from 'path' -import { fileURLToPath } from 'url' -import { ServerConfig } from './test-utils.js' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import type { ServerConfig } from './test-utils.js' const __dirname = dirname(fileURLToPath(import.meta.url)) const rootDir = join(__dirname, '../..') @@ -19,7 +19,9 @@ export const SERVER_CONFIGS: ServerConfig[] = [ transportTypes: ['auto', 'http'], }, ], - expectedTools: 1, + expectedTools: 2, + expectedResources: 2, // 2 direct resources (templates are counted separately) + expectedPrompts: 2, }, { name: 'cf-agents', @@ -43,6 +45,8 @@ export const SERVER_CONFIGS: ServerConfig[] = [ transportTypes: ['auto', 'sse'], }, ], - expectedTools: 1, + expectedTools: 2, + expectedResources: 3, // 3 direct resources (no templates in cf-agents) + expectedPrompts: 2, }, ] diff --git a/test/integration/test-utils.ts b/test/integration/test-utils.ts index 066e835..95d5400 100644 --- a/test/integration/test-utils.ts +++ b/test/integration/test-utils.ts @@ -1,8 +1,8 @@ -import { spawn, ChildProcess } from 'child_process' -import { Page } from 'playwright' -import { readFileSync } from 'fs' -import { join, dirname } from 'path' -import { fileURLToPath } from 'url' +import { spawn, type ChildProcess } from 'node:child_process' +import type { Page } from 'playwright' +import { readFileSync } from 'node:fs' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' const __dirname = dirname(fileURLToPath(import.meta.url)) const testDir = join(__dirname, '..') @@ -25,6 +25,8 @@ export interface ServerConfig { portKey: keyof TestState endpoints: ServerEndpoint[] expectedTools: number + expectedResources?: number + expectedPrompts?: number } /** @@ -132,7 +134,7 @@ export async function connectToMCPServer( page: Page, serverUrl: string, 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() if (!state.staticPort) { @@ -197,7 +199,9 @@ export async function connectToMCPServer( // Extract available tools const tools: string[] = [] 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() for (let i = 0; i < toolCount; i++) { @@ -211,6 +215,44 @@ export async function connectToMCPServer( 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 let debugLog = '' try { @@ -225,6 +267,8 @@ export async function connectToMCPServer( return { success: isConnected, tools, + resources, + prompts, debugLog, } } From 6a42f9dc93380a4cf1b9e9c393fcdbd70cac7193 Mon Sep 17 00:00:00 2001 From: Jem <591643+jem-computer@users.noreply.github.com> Date: Wed, 2 Jul 2025 18:24:03 -0700 Subject: [PATCH 7/7] feat(examples): transform hono-mcp server to Vengabus party theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transform the generic example server into a fun Vengabus-themed MCP server with: - Replace generic tools with party-themed tools (party capacity calculator, vengabus schedule) - Update resources to use party-themed URIs and content - Replace generic prompts with party invitations and announcements - Fix resource URI handling to strip trailing slashes - Fix TypeScript errors with proper resource template typing - Use docs:// protocol for party manual resource 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- examples/servers/hono-mcp/src/index.ts | 208 +++++++++++++++---------- 1 file changed, 124 insertions(+), 84 deletions(-) diff --git a/examples/servers/hono-mcp/src/index.ts b/examples/servers/hono-mcp/src/index.ts index da2ba96..c698efb 100644 --- a/examples/servers/hono-mcp/src/index.ts +++ b/examples/servers/hono-mcp/src/index.ts @@ -1,4 +1,4 @@ -import { McpServer, type ReadResourceTemplateCallback, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js' +import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js' import { StreamableHTTPTransport } from '@hono/mcp' import { Hono } from 'hono' import { z } from 'zod' @@ -17,19 +17,27 @@ app.use( // Your MCP server implementation const mcpServer = new McpServer({ - name: 'my-mcp-server', + name: 'vengabus-mcp-server', version: '1.0.0', }) mcpServer.registerTool( - 'add', + 'calculate_party_capacity', { - title: 'Addition Tool', - description: 'Add two numbers', - inputSchema: { a: z.number(), b: z.number() }, + title: 'Party Bus Capacity Calculator', + description: 'Calculate how many people can fit on the Vengabus', + inputSchema: { + busCount: z.number().describe('Number of Vengabuses'), + peoplePerBus: z.number().describe('People per bus'), + }, }, - async ({ a, b }) => ({ - content: [{ type: 'text', text: String(a + b) }], + async ({ busCount, peoplePerBus }) => ({ + content: [ + { + type: 'text', + text: `🚌 ${busCount} Vengabuses can transport ${busCount * peoplePerBus} party people! The wheels of steel are turning! 🎉`, + }, + ], }), ) @@ -48,7 +56,22 @@ mcpServer.registerTool( content: [ { 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! 🏝️`, }, ], } @@ -58,107 +81,124 @@ mcpServer.registerTool( // Register sample resources mcpServer.registerResource( 'config', - 'config://app', + 'config://vengabus', { - title: 'Application Configuration', - description: 'Current application configuration settings', + title: 'Vengabus Fleet Configuration', + description: 'Current Vengabus fleet settings and party parameters', mimeType: 'application/json', }, - async (uri: URL) => ({ - contents: [ - { - uri: uri.href, - mimeType: 'application/json', - text: JSON.stringify( - { - version: '1.0.0', - environment: 'test', - features: { - vengabus: true, - calculator: true, + 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, - ), - }, - ], - }), + null, + 2, + ), + }, + ], + } + }, ) mcpServer.registerResource( 'readme', - 'file://readme.md', + 'docs://party-manual.md', { - title: 'README', - description: 'Server documentation', + title: 'Vengabus Party Manual', + description: 'Official guide to the Vengabus experience', mimeType: 'text/markdown', }, - async (uri: URL) => ({ - contents: [ - { - uri: uri.href, - mimeType: 'text/markdown', - text: '# Test MCP Server\n\nThis is a test server for the use-mcp integration tests.\n\n## Features\n- Addition tool\n- Vengabus schedule checker\n- Sample resources\n- Sample prompts', - }, - ], + () => ({ + + 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('data://stats/{type}', { list: undefined }), + new ResourceTemplate('party://stats/{metric}', { list: undefined }), { - title: 'Statistics Data', - description: 'Get statistics for different types', + title: 'Vengabus Party Statistics', + description: 'Get party metrics (bpm, passengers, energy)', mimeType: 'application/json', }, - // @ts-expect-error - type is missing in the callback - async (uri: URL, { type }) => ({ - contents: [ - { - uri: uri.href, - mimeType: 'application/json', - text: JSON.stringify( - { - type: type, - count: Math.floor(Math.random() * 100), - lastUpdated: new Date().toISOString(), - }, - null, - 2, - ), - }, - ], - }), + 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( - 'greeting', + 'party_invitation', { - title: 'Generate Greeting', - description: 'Generate a greeting message', + title: 'Vengabus Party Invitation', + description: 'Generate a party invitation for the Vengabus', argsSchema: { - name: z.string().describe('Name of the person to greet'), - style: z.string().optional().describe('Style of greeting (formal/casual)'), + name: z.string().describe('Name of the party person'), + destination: z.string().optional().describe('Where the Vengabus is heading'), }, }, - ({ name, style }) => ({ + ({ name, destination }) => ({ messages: [ { role: 'user', content: { type: 'text', - text: `Generate a ${style || 'casual'} greeting for ${name}`, - }, - }, - { - role: 'assistant', - content: { - type: 'text', - text: style === 'formal' ? `Good day, ${name}. I hope this message finds you well.` : `Hey ${name}! How's it going?`, + text: `Create a party invitation for ${name} to join the Vengabus${destination ? ` heading to ${destination}` : ''}`, }, }, ], @@ -166,22 +206,22 @@ mcpServer.registerPrompt( ) mcpServer.registerPrompt( - 'code_review', + 'party_announcement', { - title: 'Code Review Template', - description: 'Template for code review requests', + title: 'Party Bus Announcement', + description: 'Generate party-themed announcements for various occasions', argsSchema: { - language: z.string().describe('Programming language'), - focus: z.string().optional().describe('What to focus on (performance/security/style)'), + event: z.string().describe('Type of event (deployment, release, milestone, etc.)'), + details: z.string().optional().describe('Additional details about the event'), }, }, - ({ language, focus }) => ({ + ({ event, details }) => ({ messages: [ { role: 'user', content: { type: 'text', - text: `Please review the following ${language} code${focus ? ` with a focus on ${focus}` : ''}:`, + text: `Create a party bus announcement for a ${event}${details ? `: ${details}` : ''}`, }, }, ],