) => Promise.resolve({ messages: [] }),
clearStorage: () => {},
})
}
@@ -460,6 +489,218 @@ 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 (
+
+
+
+
setExpandedResources((prev) => ({ ...prev, [resource.uri]: !isExpanded }))}
+ className="text-gray-500 hover:text-gray-700 mt-0.5"
+ >
+ {isExpanded ? : }
+
+
+
+
+
{resource.name}
+
+ {resource.description &&
{resource.description}
}
+
{resource.uri}
+ {resource.mimeType &&
({resource.mimeType}) }
+
+
+
+ {isExpanded && (
+
+
{
+ 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
+
+
+ {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 (
+
+
+
+
setExpandedPrompts((prev) => ({ ...prev, [prompt.name]: !isExpanded }))}
+ className="text-gray-500 hover:text-gray-700 mt-0.5"
+ >
+ {isExpanded ? : }
+
+
+
+
+
{prompt.name}
+
+ {prompt.description &&
{prompt.description}
}
+
+
+
+ {isExpanded && (
+
+ {/* Prompt Arguments */}
+ {prompt.arguments && prompt.arguments.length > 0 && (
+
+
Arguments:
+ {prompt.arguments.map((arg: any) => (
+
+
+ {arg.name}
+ {arg.required && * }
+ {arg.description && ({arg.description}) }
+
+
+ setPromptArgs((prev) => ({
+ ...prev,
+ [prompt.name]: {
+ ...prev[prompt.name],
+ [arg.name]: e.target.value,
+ },
+ }))
+ }
+ placeholder={arg.description || `Enter ${arg.name}`}
+ />
+
+ ))}
+
+ )}
+
+
{
+ 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
+
+
+ {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 */}
Debug Log
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..c698efb 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, 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! 🎉`,
+ },
+ ],
}),
)
@@ -42,18 +50,184 @@ 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: [
{
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) => {
const transport = new StreamableHTTPTransport()
await mcpServer.connect(transport)
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() {
Clear Stored Auth
+
+ {/* Example: Working with Resources */}
+ {mcp.state === 'ready' && mcp.resources.length > 0 && (
+
+
Available Resources:
+ {mcp.resources.map((resource) => (
+
+ {resource.name}
+ {
+ try {
+ const content = await mcp.readResource(resource.uri)
+ console.log('Resource content:', content)
+ } catch (error) {
+ console.error('Failed to read resource:', error)
+ }
+ }}>
+ Read
+
+
+ ))}
+
+ )}
+
+ {/* Example: Working with Prompts */}
+ {mcp.state === 'ready' && mcp.prompts.length > 0 && (
+
+
Available Prompts:
+ {mcp.prompts.map((prompt) => (
+
+ {prompt.name}
+ {
+ 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
+
+
+ ))}
+
+ )}
)
}
@@ -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.
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'
diff --git a/src/react/types.ts b/src/react/types.ts
index 9ac9aba..61934ea 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, Prompt } from '@modelcontextprotocol/sdk/types.js'
export type UseMcpOptions = {
/** The /sse URL of your remote MCP server */
@@ -35,6 +35,12 @@ 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[]
+ /** 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).
@@ -63,6 +69,36 @@ 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 }> }>
+ /**
+ * 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 8645a82..f95b4f2 100644
--- a/src/react/useMcp.ts
+++ b/src/react/useMcp.ts
@@ -1,5 +1,17 @@
// 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 both transport types
import { SSEClientTransport, SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js'
@@ -39,6 +51,9 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
const [state, setState] = useState('discovering')
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)
@@ -95,6 +110,9 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
if (isMountedRef.current && !quiet) {
setState('discovering')
setTools([])
+ setResources([])
+ setResourceTemplates([])
+ setPrompts([])
setError(undefined)
setAuthUrl(undefined)
}
@@ -283,16 +301,49 @@ 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, resources, and prompts...`)
successfulTransportRef.current = transportType // Store successful type
setState('loading')
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) {
// 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 : [])
+ 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
// connectingRef will be set to false after orchestration logic
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
+ // 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) => {
+ // 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)
@@ -691,10 +824,17 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
return {
state,
tools,
+ resources,
+ resourceTemplates,
+ prompts,
error,
log,
authUrl,
callTool,
+ listResources,
+ readResource,
+ listPrompts,
+ getPrompt,
retry,
disconnect,
authenticate,
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,
}
}