Transfer from geelen/use-mcp
This commit is contained in:
parent
d1d7803d9a
commit
de192f4de0
45 changed files with 9512 additions and 136 deletions
25
.github/workflows/check.yml
vendored
Normal file
25
.github/workflows/check.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
name: Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup pnpm & install
|
||||||
|
uses: wyvox/action-setup-pnpm@v3
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Run checks
|
||||||
|
run: pnpm run check
|
||||||
26
.github/workflows/publish.yml
vendored
Normal file
26
.github/workflows/publish.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
name: Publish Any Commit
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
tags:
|
||||||
|
- '!**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup pnpm & install
|
||||||
|
uses: wyvox/action-setup-pnpm@v3
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- run: pnpm dlx pkg-pr-new publish --bin
|
||||||
140
.gitignore
vendored
140
.gitignore
vendored
|
|
@ -1,136 +1,8 @@
|
||||||
# Logs
|
node_modules
|
||||||
logs
|
.mcp-cli
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
|
||||||
bower_components
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# Snowpack dependency directory (https://snowpack.dev/)
|
|
||||||
web_modules/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Optional stylelint cache
|
|
||||||
.stylelintcache
|
|
||||||
|
|
||||||
# Microbundle cache
|
|
||||||
.rpt2_cache/
|
|
||||||
.rts2_cache_cjs/
|
|
||||||
.rts2_cache_es/
|
|
||||||
.rts2_cache_umd/
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variable files
|
|
||||||
.env
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
.env.local
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
.parcel-cache
|
|
||||||
|
|
||||||
# Next.js build output
|
|
||||||
.next
|
|
||||||
out
|
|
||||||
|
|
||||||
# Nuxt.js build / generate output
|
|
||||||
.nuxt
|
|
||||||
dist
|
dist
|
||||||
|
.env
|
||||||
|
.aider*
|
||||||
|
|
||||||
# Gatsby files
|
**/.claude/settings.local.json
|
||||||
.cache/
|
public
|
||||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
|
||||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
|
||||||
# public
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# vuepress v2.x temp and cache directory
|
|
||||||
.temp
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# vitepress build output
|
|
||||||
**/.vitepress/dist
|
|
||||||
|
|
||||||
# vitepress cache directory
|
|
||||||
**/.vitepress/cache
|
|
||||||
|
|
||||||
# Docusaurus cache and generated files
|
|
||||||
.docusaurus
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
# TernJS port file
|
|
||||||
.tern-port
|
|
||||||
|
|
||||||
# Stores VSCode versions used for testing VSCode extensions
|
|
||||||
.vscode-test
|
|
||||||
|
|
||||||
# yarn v2
|
|
||||||
.yarn/cache
|
|
||||||
.yarn/unplugged
|
|
||||||
.yarn/build-state.yml
|
|
||||||
.yarn/install-state.gz
|
|
||||||
.pnp.*
|
|
||||||
|
|
|
||||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
pnpm-lock.yaml
|
||||||
|
*.md
|
||||||
|
examples
|
||||||
14
.prettierrc
Normal file
14
.prettierrc
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"printWidth": 140,
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"useTabs": false,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.jsonc"],
|
||||||
|
"options": {
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
32
AGENT.md
Normal file
32
AGENT.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# use-mcp Project Guidelines
|
||||||
|
|
||||||
|
## Build, Lint, and Test Commands
|
||||||
|
- `pnpm dev`: Run development build with watch mode
|
||||||
|
- `pnpm build`: Build the project
|
||||||
|
- `pnpm check`: Run prettier checks and TypeScript type checking
|
||||||
|
|
||||||
|
## Code Style Guidelines
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
- Use explicit .js extensions in imports (ES modules style)
|
||||||
|
- Group imports: SDK imports first, followed by React/external deps, then local imports
|
||||||
|
|
||||||
|
### Formatting
|
||||||
|
- Single quotes for strings
|
||||||
|
- No semicolons at line ends
|
||||||
|
- 140 character line width
|
||||||
|
- Use 2 space indentation
|
||||||
|
|
||||||
|
### Types and Naming
|
||||||
|
- Strong typing with TypeScript
|
||||||
|
- Descriptive interface names with camelCase for variables/functions and PascalCase for types
|
||||||
|
- Comprehensive JSDoc comments for public API functions and types
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Use assertions with descriptive messages
|
||||||
|
- Log errors with appropriate levels (debug, info, warn, error)
|
||||||
|
- Defensive error handling with specific error types when available
|
||||||
|
|
||||||
|
### React Patterns
|
||||||
|
- Use React hooks with useRef for mutable values
|
||||||
|
- Stable callbacks with useCallback and appropriate dependencies
|
||||||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 Glen Maddern
|
Copyright (c) 2025 Cloudflare, Inc.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
||||||
189
README.md
189
README.md
|
|
@ -1 +1,188 @@
|
||||||
# mcp-remote
|
<div class="oranda-hide">
|
||||||
|
|
||||||
|
# 🦑 use-mcp 🦑
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
[](https://github.com/modelcontextprotocol/use-mcp) [](https://www.npmjs.com/package/use-mcp) 
|
||||||
|
|
||||||
|
A lightweight React hook for connecting to [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol) servers. Simplifies authentication and tool calling for AI systems implementing the MCP standard.
|
||||||
|
|
||||||
|
Try it out: [MCP Inspector](https://inspector.use-mcp.dev) | [Cloudflare Workers AI Playground](https://playground.ai.cloudflare.com/)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install use-mcp
|
||||||
|
# or
|
||||||
|
pnpm add use-mcp
|
||||||
|
# or
|
||||||
|
yarn add use-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🔄 Automatic connection management with reconnection and retries
|
||||||
|
- 🔐 OAuth authentication flow handling with popup and fallback support
|
||||||
|
- 📦 Simple React hook interface for MCP integration
|
||||||
|
- 🧰 TypeScript types for editor assistance and type checking
|
||||||
|
- 📝 Comprehensive logging for debugging
|
||||||
|
- 🌐 Works with both HTTP and SSE (Server-Sent Events) transports
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useMcp } from 'use-mcp/react'
|
||||||
|
|
||||||
|
function MyAIComponent() {
|
||||||
|
const {
|
||||||
|
state, // Connection state: 'discovering' | 'authenticating' | 'connecting' | 'loading' | 'ready' | 'failed'
|
||||||
|
tools, // Available tools from MCP server
|
||||||
|
error, // Error message if connection failed
|
||||||
|
callTool, // Function to call tools on the MCP server
|
||||||
|
retry, // Retry connection manually
|
||||||
|
authenticate, // Manually trigger authentication
|
||||||
|
clearStorage, // Clear stored tokens and credentials
|
||||||
|
} = useMcp({
|
||||||
|
url: 'https://your-mcp-server.com',
|
||||||
|
clientName: 'My App',
|
||||||
|
autoReconnect: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle different states
|
||||||
|
if (state === 'failed') {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Connection failed: {error}</p>
|
||||||
|
<button onClick={retry}>Retry</button>
|
||||||
|
<button onClick={authenticate}>Authenticate Manually</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state !== 'ready') {
|
||||||
|
return <div>Connecting to AI service...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use available tools
|
||||||
|
const handleSearch = async () => {
|
||||||
|
try {
|
||||||
|
const result = await callTool('search', { query: 'example search' })
|
||||||
|
console.log('Search results:', result)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Tool call failed:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Available Tools: {tools.length}</h2>
|
||||||
|
<ul>
|
||||||
|
{tools.map(tool => (
|
||||||
|
<li key={tool.name}>{tool.name}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<button onClick={handleSearch}>Search</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting Up OAuth Callback
|
||||||
|
|
||||||
|
To handle the OAuth authentication flow, you need to set up a callback endpoint in your app.
|
||||||
|
|
||||||
|
### With React Router
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// App.tsx with React Router
|
||||||
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { onMcpAuthorization } from 'use-mcp'
|
||||||
|
|
||||||
|
function OAuthCallback() {
|
||||||
|
useEffect(() => {
|
||||||
|
onMcpAuthorization()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Authenticating...</h1>
|
||||||
|
<p>This window should close automatically.</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/oauth/callback" element={<OAuthCallback />} />
|
||||||
|
<Route path="/" element={<YourMainComponent />} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Next.js Pages Router
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// pages/oauth/callback.tsx
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { onMcpAuthorization } from 'use-mcp'
|
||||||
|
|
||||||
|
export default function OAuthCallbackPage() {
|
||||||
|
useEffect(() => {
|
||||||
|
onMcpAuthorization()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Authenticating...</h1>
|
||||||
|
<p>This window should close automatically.</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### `useMcp` Hook
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function useMcp(options: UseMcpOptions): UseMcpResult
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
| Option | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `url` | `string` | **Required**. URL of your MCP server |
|
||||||
|
| `clientName` | `string` | Name of your client for OAuth registration |
|
||||||
|
| `clientUri` | `string` | URI of your client for OAuth registration |
|
||||||
|
| `callbackUrl` | `string` | Custom callback URL for OAuth redirect (defaults to `/oauth/callback` on the current origin) |
|
||||||
|
| `storageKeyPrefix` | `string` | Storage key prefix for OAuth data in localStorage (defaults to "mcp:auth") |
|
||||||
|
| `clientConfig` | `object` | Custom configuration for the MCP client identity |
|
||||||
|
| `debug` | `boolean` | Whether to enable verbose debug logging |
|
||||||
|
| `autoRetry` | `boolean \| number` | Auto retry connection if initial connection fails, with delay in ms |
|
||||||
|
| `autoReconnect` | `boolean \| number` | Auto reconnect if an established connection is lost, with delay in ms (default: 3000) |
|
||||||
|
|
||||||
|
#### Return Value
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `state` | `string` | Current connection state: 'discovering', 'authenticating', 'connecting', 'loading', 'ready', 'failed' |
|
||||||
|
| `tools` | `Tool[]` | Available tools 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<string, unknown>) => Promise<any>` | Function to call a tool on the MCP server |
|
||||||
|
| `retry` | `() => void` | Manually attempt to reconnect |
|
||||||
|
| `disconnect` | `() => void` | Disconnect from the MCP server |
|
||||||
|
| `authenticate` | `() => void` | Manually trigger authentication |
|
||||||
|
| `clearStorage` | `() => void` | Clear all stored authentication data |
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
30
examples/inspector/.gitignore
vendored
Normal file
30
examples/inspector/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
.wrangler
|
||||||
|
|
||||||
|
# Playwright test results
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
7
examples/inspector/.prettierrc
Normal file
7
examples/inspector/.prettierrc
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 140
|
||||||
|
}
|
||||||
25
examples/inspector/AGENT.md
Normal file
25
examples/inspector/AGENT.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# AI Chat Template - Development Guide
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
- **Dev server**: `pnpm dev`
|
||||||
|
- **Build**: `pnpm build` (runs TypeScript compilation then Vite build)
|
||||||
|
- **Lint**: `pnpm lint` (ESLint)
|
||||||
|
- **Deploy**: `pnpm deploy` (builds and deploys with Wrangler)
|
||||||
|
- **Test**: `pnpm test` (Playwright E2E tests)
|
||||||
|
- **Test UI**: `pnpm test:ui` (Playwright test runner with UI)
|
||||||
|
- **Test headed**: `pnpm test:headed` (Run tests in visible browser)
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
- **Formatting**: Prettier with 2-space tabs, single quotes, no semicolons, 140 char line width
|
||||||
|
- **Imports**: Use `.tsx`/`.ts` extensions in imports, group by external/internal
|
||||||
|
- **Components**: React FC with explicit typing, PascalCase names
|
||||||
|
- **Hooks**: Custom hooks start with `use`, camelCase
|
||||||
|
- **Types**: Define interfaces in `src/types/index.ts`, use `type` for unions
|
||||||
|
- **Files**: Use PascalCase for components, camelCase for hooks/utilities
|
||||||
|
- **State**: Use proper TypeScript typing for all state variables
|
||||||
|
- **Error handling**: Use try/catch blocks with proper error propagation
|
||||||
|
- **Database**: IndexedDB with typed interfaces, async/await pattern
|
||||||
|
- **Styling**: Tailwind CSS classes, responsive design patterns
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
React 19, TypeScript, Vite, Tailwind CSS, Hono API, Cloudflare Workers, IndexedDB
|
||||||
52
examples/inspector/README.md
Normal file
52
examples/inspector/README.md
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# MCP Inspector
|
||||||
|
|
||||||
|
A minimal demo showcasing the `use-mcp` React hook for connecting to Model Context Protocol (MCP) servers.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Connect to any MCP server via URL
|
||||||
|
- View available tools and their schemas
|
||||||
|
- Real-time connection status monitoring
|
||||||
|
- Debug logging for troubleshooting
|
||||||
|
- Clean, minimal UI focused on MCP functionality
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
0. Make sure you've built the parent `use-mcp` directory at least once!
|
||||||
|
```bash
|
||||||
|
cd ../.. && pnpm build && cd -
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, run `pnpm dev` in the parent directory in a second terminal if you want to iterate on both the library and the example together.
|
||||||
|
|
||||||
|
1. Install dependencies:
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start the development server:
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
|
||||||
|
## What This Demonstrates
|
||||||
|
|
||||||
|
This example shows how easy it is to integrate MCP servers into a React application using the `use-mcp` hook. The core functionality is just:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useMcp } from 'use-mcp/react'
|
||||||
|
|
||||||
|
const connection = useMcp({
|
||||||
|
url: 'your-mcp-server-url',
|
||||||
|
debug: true,
|
||||||
|
autoRetry: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Access connection.state, connection.tools, connection.error, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
The `McpServers` component wraps this hook to provide a complete UI for server management and tool inspection.
|
||||||
28
examples/inspector/eslint.config.js
Normal file
28
examples/inspector/eslint.config.js
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
43
examples/inspector/index.html
Normal file
43
examples/inspector/index.html
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<!-- Primary Meta Tags -->
|
||||||
|
<title>MCP Inspector - use-mcp React Hook Demo</title>
|
||||||
|
<meta name="description" content="Interactive demo of the use-mcp React hook for Model Context Protocol (MCP) servers. Connect to MCP servers, explore available tools, and execute commands using the use-mcp library." />
|
||||||
|
|
||||||
|
<!-- Open Graph / Facebook -->
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:site_name" content="MCP Inspector" />
|
||||||
|
<meta property="og:title" content="MCP Inspector - use-mcp React Hook Demo" />
|
||||||
|
<meta property="og:description" content="Interactive demo of the use-mcp React hook for Model Context Protocol (MCP) servers. Connect to MCP servers, explore available tools, and execute commands using the use-mcp library." />
|
||||||
|
<meta property="og:image" content="https://inspector.use-mcp.dev/use-mcp-card.png" />
|
||||||
|
<meta property="og:image:width" content="1200" />
|
||||||
|
<meta property="og:image:height" content="630" />
|
||||||
|
<meta property="og:url" content="https://inspector.use-mcp.dev" />
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="MCP Inspector - use-mcp React Hook Demo" />
|
||||||
|
<meta name="twitter:description" content="Interactive demo of the use-mcp React hook for Model Context Protocol (MCP) servers. Connect to MCP servers, explore available tools, and execute commands using the use-mcp library." />
|
||||||
|
<meta name="twitter:image" content="https://inspector.use-mcp.dev/use-mcp-card.png" />
|
||||||
|
|
||||||
|
<!-- Additional Meta Tags -->
|
||||||
|
<meta name="author" content="Glen Maddern" />
|
||||||
|
<meta name="keywords" content="use-mcp, Model Context Protocol, MCP, AI, LLM, tools, inspector, debugging, development, React hook" />
|
||||||
|
<meta name="application-name" content="MCP Inspector" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="MCP Inspector" />
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
<!-- Canonical URL -->
|
||||||
|
<link rel="canonical" href="https://inspector.use-mcp.dev" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
41
examples/inspector/package.json
Normal file
41
examples/inspector/package.json
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"name": "mcp-inspector",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && pnpm run build:only",
|
||||||
|
"build:only": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"deploy": "pnpm run build:only && wrangler deploy"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.0.14",
|
||||||
|
"lucide-react": "^0.482.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.6.2",
|
||||||
|
"tailwindcss": "^4.0.14",
|
||||||
|
"use-mcp": "link:../.."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@cloudflare/vite-plugin": "^1.5.0",
|
||||||
|
"@eslint/js": "^9.21.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
|
"@types/react": "^19.0.10",
|
||||||
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"eslint": "^9.21.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
|
"globals": "^15.15.0",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"typescript": "~5.7.2",
|
||||||
|
"typescript-eslint": "^8.24.1",
|
||||||
|
"vite": "^6.2.0",
|
||||||
|
"wrangler": "^4.19.1"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
|
||||||
|
}
|
||||||
3795
examples/inspector/pnpm-lock.yaml
generated
Normal file
3795
examples/inspector/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
1
examples/inspector/public/vite.svg
Normal file
1
examples/inspector/public/vite.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
37
examples/inspector/src/App.tsx
Normal file
37
examples/inspector/src/App.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
|
||||||
|
import { McpServers } from './components/McpServers.js'
|
||||||
|
import { OAuthCallback } from './components/OAuthCallback.js'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/oauth/callback" element={<OAuthCallback />} />
|
||||||
|
<Route path="/" element={
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||||
|
<div className="w-full max-w-2xl">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">MCP Inspector</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Minimal demo showcasing the{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/modelcontextprotocol/use-mcp"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:text-blue-800 underline font-medium transition-colors"
|
||||||
|
>
|
||||||
|
use-mcp
|
||||||
|
</a>{' '}
|
||||||
|
React hook
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<McpServers />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
557
examples/inspector/src/components/McpServers.tsx
Normal file
557
examples/inspector/src/components/McpServers.tsx
Normal file
|
|
@ -0,0 +1,557 @@
|
||||||
|
import { useState, useRef, useEffect } from 'react'
|
||||||
|
import { useMcp, type Tool } from 'use-mcp/react'
|
||||||
|
import { Info, X, ChevronRight, ChevronDown } from 'lucide-react'
|
||||||
|
|
||||||
|
|
||||||
|
// MCP Connection wrapper that only renders when active
|
||||||
|
function McpConnection({
|
||||||
|
serverUrl,
|
||||||
|
onConnectionUpdate,
|
||||||
|
}: {
|
||||||
|
serverUrl: string
|
||||||
|
onConnectionUpdate: (data: any) => void
|
||||||
|
}) {
|
||||||
|
// Use the MCP hook with the server URL
|
||||||
|
const connection = useMcp({
|
||||||
|
url: serverUrl,
|
||||||
|
debug: true,
|
||||||
|
autoRetry: false,
|
||||||
|
popupFeatures: 'width=500,height=600,resizable=yes,scrollbars=yes',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update parent component with connection data
|
||||||
|
useEffect(() => {
|
||||||
|
onConnectionUpdate(connection)
|
||||||
|
}, [
|
||||||
|
connection.state,
|
||||||
|
connection.tools,
|
||||||
|
connection.error,
|
||||||
|
connection.log.length,
|
||||||
|
connection.authUrl,
|
||||||
|
])
|
||||||
|
|
||||||
|
// Return null as this is just a hook wrapper
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function McpServers({
|
||||||
|
onToolsUpdate,
|
||||||
|
}: {
|
||||||
|
onToolsUpdate?: (tools: Tool[]) => void
|
||||||
|
}) {
|
||||||
|
const [serverUrl, setServerUrl] = useState(() => {
|
||||||
|
return sessionStorage.getItem('mcpServerUrl') || ''
|
||||||
|
})
|
||||||
|
const [isActive, setIsActive] = useState(false)
|
||||||
|
|
||||||
|
const [connectionData, setConnectionData] = useState<any>({
|
||||||
|
state: 'not-connected',
|
||||||
|
tools: [],
|
||||||
|
error: undefined,
|
||||||
|
log: [],
|
||||||
|
authUrl: undefined,
|
||||||
|
retry: () => {},
|
||||||
|
disconnect: () => {},
|
||||||
|
authenticate: () => Promise.resolve(undefined),
|
||||||
|
callTool: (_name: string, _args?: Record<string, unknown>) =>
|
||||||
|
Promise.resolve(undefined),
|
||||||
|
clearStorage: () => {},
|
||||||
|
})
|
||||||
|
const [toolForms, setToolForms] = useState<Record<string, Record<string, any>>>({})
|
||||||
|
const [toolExecutionLogs, setToolExecutionLogs] = useState<Record<string, string>>({})
|
||||||
|
const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>({})
|
||||||
|
const logRef = useRef<HTMLDivElement>(null)
|
||||||
|
const executionLogRefs = useRef<Record<string, HTMLTextAreaElement | null>>({})
|
||||||
|
|
||||||
|
// Extract connection properties
|
||||||
|
const { state, tools, log, authUrl, disconnect, authenticate } =
|
||||||
|
connectionData
|
||||||
|
|
||||||
|
// Notify parent component when tools change
|
||||||
|
useEffect(() => {
|
||||||
|
if (onToolsUpdate && tools.length > 0) {
|
||||||
|
onToolsUpdate(
|
||||||
|
tools.map((t: Tool) => ({
|
||||||
|
...t,
|
||||||
|
callTool: (args: Record<string, unknown>) =>
|
||||||
|
connectionData.callTool(t.name, args),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [tools, onToolsUpdate])
|
||||||
|
|
||||||
|
// Handle connection
|
||||||
|
const handleConnect = () => {
|
||||||
|
if (!serverUrl.trim()) return
|
||||||
|
setIsActive(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle disconnection
|
||||||
|
const handleDisconnect = () => {
|
||||||
|
disconnect()
|
||||||
|
setIsActive(false)
|
||||||
|
setConnectionData({
|
||||||
|
state: 'not-connected',
|
||||||
|
tools: [],
|
||||||
|
error: undefined,
|
||||||
|
log: [],
|
||||||
|
authUrl: undefined,
|
||||||
|
retry: () => {},
|
||||||
|
disconnect: () => {},
|
||||||
|
authenticate: () => Promise.resolve(undefined),
|
||||||
|
callTool: (_name: string, _args?: Record<string, unknown>) =>
|
||||||
|
Promise.resolve(undefined),
|
||||||
|
clearStorage: () => {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle authentication if popup was blocked
|
||||||
|
const handleManualAuth = async () => {
|
||||||
|
try {
|
||||||
|
await authenticate()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Authentication error:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-scroll log to bottom
|
||||||
|
useEffect(() => {
|
||||||
|
if (logRef.current) {
|
||||||
|
logRef.current.scrollTop = logRef.current.scrollHeight
|
||||||
|
}
|
||||||
|
}, [log])
|
||||||
|
|
||||||
|
// Generate status badge based on connection state
|
||||||
|
const getStatusBadge = () => {
|
||||||
|
const baseClasses = 'px-2 py-1 rounded-full text-xs font-medium'
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case 'discovering':
|
||||||
|
return (
|
||||||
|
<span className={`${baseClasses} bg-blue-100 text-blue-800`}>
|
||||||
|
Discovering
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
case 'authenticating':
|
||||||
|
return (
|
||||||
|
<span className={`${baseClasses} bg-purple-100 text-purple-800`}>
|
||||||
|
Authenticating
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
case 'connecting':
|
||||||
|
return (
|
||||||
|
<span className={`${baseClasses} bg-yellow-100 text-yellow-800`}>
|
||||||
|
Connecting
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
case 'loading':
|
||||||
|
return (
|
||||||
|
<span className={`${baseClasses} bg-orange-100 text-orange-800`}>
|
||||||
|
Loading
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
case 'ready':
|
||||||
|
return (
|
||||||
|
<span className={`${baseClasses} bg-green-100 text-green-800`}>
|
||||||
|
Connected
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
case 'failed':
|
||||||
|
return (
|
||||||
|
<span className={`${baseClasses} bg-red-100 text-red-800`}>
|
||||||
|
Failed
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
case 'not-connected':
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<span className={`${baseClasses} bg-gray-100 text-gray-800`}>
|
||||||
|
Not Connected
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the tools to console when they change (for debugging)
|
||||||
|
useEffect(() => {
|
||||||
|
if (tools.length > 0) {
|
||||||
|
console.log('MCP Tools available:', tools)
|
||||||
|
}
|
||||||
|
}, [tools])
|
||||||
|
|
||||||
|
// Initialize form data when tools change
|
||||||
|
useEffect(() => {
|
||||||
|
const newForms: Record<string, Record<string, any>> = {}
|
||||||
|
tools.forEach((tool: Tool) => {
|
||||||
|
if (tool.inputSchema && tool.inputSchema.properties) {
|
||||||
|
const formData: Record<string, any> = {}
|
||||||
|
Object.entries(tool.inputSchema.properties).forEach(([key, schema]: [string, any]) => {
|
||||||
|
// Set default values based on type
|
||||||
|
if (schema.type === 'number' || schema.type === 'integer') {
|
||||||
|
formData[key] = schema.default || 0
|
||||||
|
} else if (schema.type === 'boolean') {
|
||||||
|
formData[key] = schema.default || false
|
||||||
|
} else {
|
||||||
|
formData[key] = schema.default || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
newForms[tool.name] = formData
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setToolForms(newForms)
|
||||||
|
}, [tools])
|
||||||
|
|
||||||
|
// Handle form input changes
|
||||||
|
const handleFormChange = (toolName: string, fieldName: string, value: any) => {
|
||||||
|
setToolForms(prev => ({
|
||||||
|
...prev,
|
||||||
|
[toolName]: {
|
||||||
|
...prev[toolName],
|
||||||
|
[fieldName]: value
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to clean and update execution log
|
||||||
|
const updateExecutionLog = (toolName: string, newContent: string) => {
|
||||||
|
setToolExecutionLogs(prev => {
|
||||||
|
const currentLog = prev[toolName] || ''
|
||||||
|
const updatedLog = currentLog + newContent
|
||||||
|
// Remove blank lines and rejoin
|
||||||
|
const cleanedLog = updatedLog
|
||||||
|
.split('\n')
|
||||||
|
.filter(line => line.trim() !== '')
|
||||||
|
.join('\n')
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[toolName]: cleanedLog + '\n'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tool execution
|
||||||
|
const handleRunTool = async (tool: Tool) => {
|
||||||
|
const args = toolForms[tool.name] || {}
|
||||||
|
const argsStr = JSON.stringify(args)
|
||||||
|
|
||||||
|
// Add execution start message
|
||||||
|
const startMessage = `Calling ${tool.name}(${argsStr})\n`
|
||||||
|
updateExecutionLog(tool.name, startMessage)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await connectionData.callTool(tool.name, args)
|
||||||
|
const resultStr = typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
||||||
|
updateExecutionLog(tool.name, `${resultStr}\n`)
|
||||||
|
} catch (error) {
|
||||||
|
updateExecutionLog(tool.name, `Error: ${error}\n`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-scroll execution logs to bottom when they change
|
||||||
|
useEffect(() => {
|
||||||
|
Object.keys(toolExecutionLogs).forEach(toolName => {
|
||||||
|
const textarea = executionLogRefs.current[toolName]
|
||||||
|
if (textarea) {
|
||||||
|
textarea.scrollTop = textarea.scrollHeight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [toolExecutionLogs])
|
||||||
|
|
||||||
|
// Clear execution log for specific tool
|
||||||
|
const clearExecutionLog = (toolName: string) => {
|
||||||
|
setToolExecutionLogs(prev => ({
|
||||||
|
...prev,
|
||||||
|
[toolName]: ''
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle tool expanded state
|
||||||
|
const toggleTool = (toolName: string) => {
|
||||||
|
setExpandedTools(prev => ({
|
||||||
|
...prev,
|
||||||
|
[toolName]: !prev[toolName]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render form field based on schema
|
||||||
|
const renderFormField = (toolName: string, fieldName: string, schema: any, isRequired: boolean) => {
|
||||||
|
const value = toolForms[toolName]?.[fieldName] || ''
|
||||||
|
|
||||||
|
if (schema.type === 'number' || schema.type === 'integer') {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="w-32 p-2 pr-6 border border-gray-200 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-300 placeholder-gray-300"
|
||||||
|
value={value}
|
||||||
|
step={schema.type === 'integer' ? 1 : 'any'}
|
||||||
|
required={isRequired}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value === '' ? '' :
|
||||||
|
(schema.type === 'integer' ? parseInt(e.target.value) || 0 : parseFloat(e.target.value) || 0)
|
||||||
|
handleFormChange(toolName, fieldName, newValue)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else if (schema.type === 'boolean') {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4 text-blue-600 rounded focus:ring-blue-300"
|
||||||
|
checked={value}
|
||||||
|
onChange={(e) => handleFormChange(toolName, fieldName, e.target.checked)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// String or other text input
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full p-2 border border-gray-200 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-300 placeholder-gray-300"
|
||||||
|
value={value}
|
||||||
|
required={isRequired}
|
||||||
|
placeholder={schema.description || ''}
|
||||||
|
onChange={(e) => handleFormChange(toolName, fieldName, e.target.value)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="rounded-lg bg-white p-4 border border-zinc-200">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-semibold">MCP Servers</span>
|
||||||
|
{getStatusBadge()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-500 text-xs mt-1 mb-3">
|
||||||
|
Connect to Model Context Protocol (MCP) servers to access additional AI
|
||||||
|
capabilities.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="flex-1 p-2 border border-gray-200 rounded text-sm placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-300"
|
||||||
|
placeholder="Enter MCP server URL"
|
||||||
|
value={serverUrl}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value
|
||||||
|
setServerUrl(newValue)
|
||||||
|
sessionStorage.setItem('mcpServerUrl', newValue)
|
||||||
|
}}
|
||||||
|
disabled={isActive && state !== 'failed'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{state === 'ready' ||
|
||||||
|
(isActive && state !== 'not-connected' && state !== 'failed') ? (
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 bg-orange-100 hover:bg-orange-200 text-orange-900 rounded text-sm font-medium whitespace-nowrap"
|
||||||
|
onClick={handleDisconnect}
|
||||||
|
>
|
||||||
|
Disconnect
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white rounded py-2 px-4 text-sm font-medium disabled:opacity-50 whitespace-nowrap"
|
||||||
|
onClick={handleConnect}
|
||||||
|
disabled={isActive || !serverUrl.trim()}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Authentication Link if needed */}
|
||||||
|
{authUrl && (
|
||||||
|
<div className="p-3 bg-orange-50 border border-orange-200 rounded">
|
||||||
|
<p className="text-xs mb-2">
|
||||||
|
Authentication required. Please click the link below:
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={authUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-xs text-orange-700 hover:text-orange-800 underline"
|
||||||
|
onClick={handleManualAuth}
|
||||||
|
>
|
||||||
|
Authenticate in new window
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Available Tools section - always present */}
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-sm mb-3">
|
||||||
|
Available Tools ({tools.length})
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{tools.length === 0 ? (
|
||||||
|
<div className="border border-gray-200 rounded p-4 bg-gray-50 text-center text-gray-500 text-sm">
|
||||||
|
No tools available. Connect to an MCP server to see available tools.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="border border-gray-200 rounded p-4 bg-gray-50 space-y-2">
|
||||||
|
{tools.map((tool: Tool, index: number) => {
|
||||||
|
const isExpanded = expandedTools[tool.name] || false
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index} className="bg-white rounded border border-gray-100 shadow-sm">
|
||||||
|
{/* Tool header - always visible */}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 p-3 cursor-pointer hover:bg-gray-50 transition-colors"
|
||||||
|
onClick={() => toggleTool(tool.name)}
|
||||||
|
>
|
||||||
|
{/* Toggle arrow */}
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown size={16} className="text-gray-500 flex-shrink-0" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight size={16} className="text-gray-500 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tool name */}
|
||||||
|
<h4 className="font-bold text-base text-black flex-shrink-0">
|
||||||
|
{tool.name}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{/* Tool description when collapsed */}
|
||||||
|
{!isExpanded && tool.description && (
|
||||||
|
<p className="text-gray-600 text-sm truncate ml-2">
|
||||||
|
{tool.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expanded content */}
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="px-3 pb-3 border-t border-gray-100">
|
||||||
|
{tool.description && (
|
||||||
|
<p className="text-gray-600 mb-4 text-sm leading-relaxed mt-3">
|
||||||
|
{tool.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Form for tool parameters */}
|
||||||
|
{tool.inputSchema && tool.inputSchema.properties && (
|
||||||
|
<div className="flex flex-wrap gap-3 mb-4">
|
||||||
|
{Object.entries(tool.inputSchema.properties).map(([fieldName, schema]: [string, any]) => {
|
||||||
|
const isRequired = Array.isArray(tool.inputSchema.required) && tool.inputSchema.required.includes(fieldName)
|
||||||
|
const isTextInput = schema.type !== 'number' && schema.type !== 'integer' && schema.type !== 'boolean'
|
||||||
|
return (
|
||||||
|
<div key={fieldName} className={`space-y-1 ${isTextInput ? 'w-full' : ''}`}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-xs font-medium text-gray-700">
|
||||||
|
{fieldName}
|
||||||
|
</label>
|
||||||
|
{schema.description && (
|
||||||
|
<div className="relative group">
|
||||||
|
<Info size={12} className="text-gray-400 cursor-help" />
|
||||||
|
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-10">
|
||||||
|
{schema.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{renderFormField(tool.name, fieldName, schema, isRequired)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Run button */}
|
||||||
|
<button
|
||||||
|
onClick={() => handleRunTool(tool)}
|
||||||
|
disabled={state !== 'ready'}
|
||||||
|
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white rounded py-2 px-4 text-sm font-medium mb-4"
|
||||||
|
>
|
||||||
|
Run
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Per-tool execution log - only show if there's content */}
|
||||||
|
{toolExecutionLogs[tool.name] && (
|
||||||
|
<div className="border-t border-gray-100 pt-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h5 className="font-medium text-sm text-gray-700">Execution Log</h5>
|
||||||
|
<button
|
||||||
|
onClick={() => clearExecutionLog(tool.name)}
|
||||||
|
className="flex items-center gap-1 text-xs text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
ref={(el) => { executionLogRefs.current[tool.name] = el }}
|
||||||
|
value={toolExecutionLogs[tool.name]}
|
||||||
|
readOnly
|
||||||
|
className="w-full h-32 p-2 border border-gray-200 rounded text-[10px] font-mono bg-gray-50 resize-none placeholder-gray-300"
|
||||||
|
placeholder="Tool execution results will appear here..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Debug Log */}
|
||||||
|
<div>
|
||||||
|
<label className="font-medium text-xs block mb-2">Debug Log</label>
|
||||||
|
<div
|
||||||
|
ref={logRef}
|
||||||
|
className="border border-gray-200 rounded p-2 bg-gray-50 h-32 overflow-y-auto font-mono text-xs"
|
||||||
|
>
|
||||||
|
{log.length > 0 ? (
|
||||||
|
log.map((entry: any, index: number) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`py-0.5 ${
|
||||||
|
entry.level === 'debug'
|
||||||
|
? 'text-gray-500'
|
||||||
|
: entry.level === 'info'
|
||||||
|
? 'text-blue-600'
|
||||||
|
: entry.level === 'warn'
|
||||||
|
? 'text-orange-600'
|
||||||
|
: 'text-red-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
[{entry.level}] {entry.message}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-gray-400">No log entries yet</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{connectionData?.state !== 'not-connected' && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
connectionData?.clearStorage()
|
||||||
|
if (isActive) {
|
||||||
|
handleDisconnect()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-xs text-orange-600 hover:text-orange-800 hover:underline mt-2"
|
||||||
|
>
|
||||||
|
Clear stored authentication
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Only render the actual MCP connection when active */}
|
||||||
|
{isActive && (
|
||||||
|
<McpConnection
|
||||||
|
serverUrl={serverUrl}
|
||||||
|
onConnectionUpdate={setConnectionData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
examples/inspector/src/components/OAuthCallback.tsx
Normal file
18
examples/inspector/src/components/OAuthCallback.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { onMcpAuthorization } from 'use-mcp'
|
||||||
|
|
||||||
|
export function OAuthCallback() {
|
||||||
|
useEffect(() => {
|
||||||
|
onMcpAuthorization()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 mb-4">Authenticating...</h1>
|
||||||
|
<p className="text-gray-600 mb-2">Please wait while we complete your authentication.</p>
|
||||||
|
<p className="text-sm text-gray-500">This window should close automatically.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue