The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .github
    └── workflows
    │   ├── check.yml
    │   ├── ci.yml
    │   └── publish.yml
├── .gitignore
├── .husky
    └── pre-commit
├── .prettierignore
├── .prettierrc
├── AGENT.md
├── LICENSE
├── README.md
├── examples
    ├── chat-ui
    │   ├── .env.example
    │   ├── .gitignore
    │   ├── .prettierrc
    │   ├── AGENT.md
    │   ├── MODEL_SELECTOR_GUIDE.md
    │   ├── README.md
    │   ├── api
    │   │   └── index.ts
    │   ├── e2e
    │   │   ├── build.spec.ts
    │   │   └── chat.spec.ts
    │   ├── eslint.config.js
    │   ├── index.html
    │   ├── package.json
    │   ├── playwright.config.ts
    │   ├── pnpm-lock.yaml
    │   ├── public
    │   │   └── vite.svg
    │   ├── scripts
    │   │   └── update-models.ts
    │   ├── src
    │   │   ├── App.tsx
    │   │   ├── components
    │   │   │   ├── ApiKeyModal.tsx
    │   │   │   ├── ChatApp.tsx
    │   │   │   ├── ChatInput.tsx
    │   │   │   ├── ChatNavbar.tsx
    │   │   │   ├── ChatSidebar.tsx
    │   │   │   ├── ConversationThread.tsx
    │   │   │   ├── McpServerModal.tsx
    │   │   │   ├── McpServers.tsx
    │   │   │   ├── ModelSelectionModal.tsx
    │   │   │   ├── ModelSelector.tsx
    │   │   │   ├── OAuthCallback.tsx
    │   │   │   ├── ProviderModelsModal.tsx
    │   │   │   └── messages
    │   │   │   │   ├── AssistantMessage.tsx
    │   │   │   │   ├── ChatMessage.tsx
    │   │   │   │   ├── ErrorMessage.tsx
    │   │   │   │   ├── ReasoningMessage.tsx
    │   │   │   │   ├── ToolCallMessage.tsx
    │   │   │   │   └── ToolResultMessage.tsx
    │   │   ├── consts.ts
    │   │   ├── data
    │   │   │   └── models.json
    │   │   ├── hooks
    │   │   │   ├── useAutoscroll.ts
    │   │   │   ├── useConversationUpdater.ts
    │   │   │   ├── useIndexedDB.ts
    │   │   │   ├── useModels.ts
    │   │   │   ├── useStreamResponse.ts
    │   │   │   └── useTheme.ts
    │   │   ├── index.css
    │   │   ├── main.tsx
    │   │   ├── styles
    │   │   │   ├── github.css
    │   │   │   ├── markdown.css
    │   │   │   └── scrollbar.css
    │   │   ├── types
    │   │   │   ├── index.ts
    │   │   │   └── models.ts
    │   │   ├── utils
    │   │   │   ├── apiKeys.ts
    │   │   │   ├── auth.ts
    │   │   │   ├── debugLog.ts
    │   │   │   ├── modelOptions.ts
    │   │   │   └── modelPreferences.ts
    │   │   └── vite-env.d.ts
    │   ├── tailwind.config.js
    │   ├── tsconfig.app.json
    │   ├── tsconfig.json
    │   ├── tsconfig.node.json
    │   ├── tsconfig.worker.json
    │   ├── vite.config.ts
    │   └── wrangler.jsonc
    ├── inspector
    │   ├── .gitignore
    │   ├── .prettierrc
    │   ├── AGENT.md
    │   ├── README.md
    │   ├── eslint.config.js
    │   ├── index.html
    │   ├── package.json
    │   ├── pnpm-lock.yaml
    │   ├── public
    │   │   └── vite.svg
    │   ├── src
    │   │   ├── App.tsx
    │   │   ├── components
    │   │   │   ├── McpServers.tsx
    │   │   │   └── OAuthCallback.tsx
    │   │   ├── index.css
    │   │   ├── main.tsx
    │   │   ├── styles
    │   │   │   ├── github.css
    │   │   │   └── scrollbar.css
    │   │   └── vite-env.d.ts
    │   ├── tailwind.config.js
    │   ├── tsconfig.app.json
    │   ├── tsconfig.json
    │   ├── tsconfig.node.json
    │   ├── vite.config.ts
    │   └── wrangler.jsonc
    └── servers
    │   ├── cf-agents
    │       ├── .gitignore
    │       ├── README.md
    │       ├── biome.json
    │       ├── package.json
    │       ├── pnpm-lock.yaml
    │       ├── src
    │       │   └── index.ts
    │       ├── tsconfig.json
    │       ├── worker-configuration.d.ts
    │       └── wrangler.jsonc
    │   └── hono-mcp
    │       ├── .gitignore
    │       ├── README.md
    │       ├── package.json
    │       ├── pnpm-lock.yaml
    │       ├── public
    │           └── .gitkeep
    │       ├── src
    │           └── index.ts
    │       ├── tsconfig.json
    │       ├── worker-configuration.d.ts
    │       └── wrangler.jsonc
├── oranda.json
├── package.json
├── pnpm-lock.yaml
├── scripts
    └── pre-commit
├── src
    ├── auth
    │   ├── browser-provider.ts
    │   ├── callback.ts
    │   └── types.ts
    ├── index.ts
    ├── react
    │   ├── README.md
    │   ├── index.ts
    │   ├── types.ts
    │   └── useMcp.ts
    └── utils
    │   └── assert.ts
├── test
    ├── README.md
    ├── integration
    │   ├── mcp-connection.test.ts
    │   ├── mcp-connection.test.ts.old
    │   ├── server-configs.ts
    │   └── test-utils.ts
    ├── package.json
    ├── pnpm-lock.yaml
    ├── setup
    │   ├── global-setup.ts
    │   └── global-teardown.ts
    ├── tsconfig.json
    └── vitest.config.ts
├── tsconfig.json
└── wrangler.jsonc


/.github/workflows/check.yml:
--------------------------------------------------------------------------------
 1 | name: Check
 2 | 
 3 | on:
 4 |   pull_request:
 5 |     branches:
 6 |       - main
 7 | 
 8 | jobs:
 9 |   check:
10 |     runs-on: ubuntu-latest
11 | 
12 |     steps:
13 |       - name: Checkout code
14 |         uses: actions/checkout@v4
15 | 
16 |       - name: Setup pnpm & install
17 |         uses: wyvox/action-setup-pnpm@v3
18 |         with:
19 |           node-version: 22
20 | 
21 |       - name: Build
22 |         run: pnpm build
23 | 
24 |       - name: Run checks
25 |         run: pnpm run check
26 | 


--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
 1 | name: CI
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [main]
 6 |   pull_request:
 7 |     branches: [main]
 8 | 
 9 | permissions:
10 |   contents: read
11 | 
12 | jobs:
13 |   test:
14 |     runs-on: ubuntu-latest
15 | 
16 |     steps:
17 |       - name: Checkout code
18 |         uses: actions/checkout@v4
19 | 
20 |       - name: Setup pnpm
21 |         uses: pnpm/action-setup@v4
22 | 
23 |       - name: Setup Node.js
24 |         uses: actions/setup-node@v4
25 |         with:
26 |           node-version: '22'
27 |           cache: 'pnpm'
28 | 
29 |       - name: Install dependencies - root
30 |         run: pnpm install
31 | 
32 |       - name: Install dependencies - inspector example
33 |         run: pnpm install
34 |         working-directory: examples/inspector
35 | 
36 |       - name: Install dependencies - cf-agents example
37 |         run: pnpm install
38 |         working-directory: examples/servers/cf-agents
39 | 
40 |       - name: Install dependencies - hono-mcp example
41 |         run: pnpm install
42 |         working-directory: examples/servers/hono-mcp
43 | 
44 |       - name: Install dependencies - test
45 |         run: pnpm install && pnpm exec playwright install
46 |         working-directory: test
47 | 
48 |       - name: Run tests
49 |         run: pnpm test:headless
50 |         working-directory: test
51 | 


--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
 1 | name: Publish Any Commit
 2 | on:
 3 |   pull_request:
 4 |   push:
 5 |     branches:
 6 |       - '**'
 7 |     tags:
 8 |       - '!**'
 9 | 
10 | jobs:
11 |   build:
12 |     runs-on: ubuntu-latest
13 | 
14 |     steps:
15 |       - name: Checkout code
16 |         uses: actions/checkout@v4
17 | 
18 |       - name: Setup pnpm & install
19 |         uses: wyvox/action-setup-pnpm@v3
20 |         with:
21 |           node-version: 22
22 | 
23 |       - name: Build
24 |         run: pnpm build
25 | 
26 |       - run: pnpm dlx pkg-pr-new publish --bin
27 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .mcp-cli
3 | dist
4 | .env
5 | .aider*
6 | 
7 | **/.claude/settings.local.json
8 | /public
9 | 


--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | ./scripts/pre-commit
2 | 


--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | pnpm-lock.yaml
2 | *.md
3 | .wrangler
4 | .vscode
5 | test-results
6 | 


--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
 1 | {
 2 |   "printWidth": 140,
 3 |   "singleQuote": true,
 4 |   "semi": false,
 5 |   "useTabs": false,
 6 |   "overrides": [
 7 |     {
 8 |       "files": ["*.jsonc"],
 9 |       "options": {
10 |         "trailingComma": "none"
11 |       }
12 |     }
13 |   ]
14 | }
15 | 


--------------------------------------------------------------------------------
/AGENT.md:
--------------------------------------------------------------------------------
 1 | # use-mcp Project Guidelines
 2 | 
 3 | ## Build, Lint, and Test Commands
 4 | - `pnpm dev`: Run development build with watch mode and start all examples/servers
 5 |   - Chat UI: http://localhost:5002
 6 |   - Inspector: http://localhost:5001
 7 |   - Hono MCP Server: http://localhost:5101
 8 |   - CF Agents MCP Server: http://localhost:5102
 9 | - `pnpm build`: Build the project
10 | - `pnpm check`: Run prettier checks and TypeScript type checking
11 | 
12 | ### Integration Tests (in /test directory)
13 | - `cd test && pnpm test`: Run integration tests headlessly (default)
14 | - `cd test && pnpm test:headed`: Run integration tests with visible browser
15 | - `cd test && pnpm test:headless`: Run integration tests headlessly 
16 | - `cd test && pnpm test:watch`: Run integration tests in watch mode
17 | - `cd test && pnpm test:ui`: Run integration tests with interactive UI
18 | 
19 | ## Code Style Guidelines
20 | 
21 | ### Imports
22 | - Use explicit .js extensions in imports (ES modules style)
23 | - Group imports: SDK imports first, followed by React/external deps, then local imports
24 | 
25 | ### Formatting
26 | - Single quotes for strings
27 | - No semicolons at line ends
28 | - 140 character line width
29 | - Use 2 space indentation
30 | 
31 | ### Types and Naming
32 | - Strong typing with TypeScript
33 | - Descriptive interface names with camelCase for variables/functions and PascalCase for types
34 | - Comprehensive JSDoc comments for public API functions and types
35 | 
36 | ### Error Handling
37 | - Use assertions with descriptive messages
38 | - Log errors with appropriate levels (debug, info, warn, error)
39 | - Defensive error handling with specific error types when available
40 | 
41 | ## Development Workflow
42 | - **Commit changes frequently** after each logical change or debugging step
43 | - Use descriptive commit messages that explain what was changed and why
44 | 
45 | ### React Patterns
46 | - Use React hooks with useRef for mutable values
47 | - Stable callbacks with useCallback and appropriate dependencies


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2025 Cloudflare, Inc.
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 


--------------------------------------------------------------------------------
/examples/chat-ui/.env.example:
--------------------------------------------------------------------------------
1 | # OpenRouter OAuth Configuration
2 | # Note: OpenRouter OAuth doesn't require client ID, only callback URL configuration
3 | VITE_OPENROUTER_REDIRECT_URI=http://localhost:5002/oauth/openrouter/callback
4 | 


--------------------------------------------------------------------------------
/examples/chat-ui/.gitignore:
--------------------------------------------------------------------------------
 1 | # Logs
 2 | logs
 3 | *.log
 4 | npm-debug.log*
 5 | yarn-debug.log*
 6 | yarn-error.log*
 7 | pnpm-debug.log*
 8 | lerna-debug.log*
 9 | 
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | 
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | 
26 | .wrangler
27 | 
28 | # Playwright test results
29 | test-results/
30 | playwright-report/


--------------------------------------------------------------------------------
/examples/chat-ui/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 |   "trailingComma": "es5",
3 |   "tabWidth": 2,
4 |   "semi": false,
5 |   "singleQuote": true,
6 |   "printWidth": 140
7 | }
8 | 


--------------------------------------------------------------------------------
/examples/chat-ui/AGENT.md:
--------------------------------------------------------------------------------
 1 | # AI Chat Template - Development Guide
 2 | 
 3 | ## Commands
 4 | - **Dev server**: `pnpm dev`
 5 | - **Build**: `pnpm build` (runs TypeScript compilation then Vite build)
 6 | - **Lint**: `pnpm lint` (ESLint)
 7 | - **Deploy**: `pnpm deploy` (builds and deploys with Wrangler)
 8 | - **Test**: `pnpm test` (Playwright E2E tests)
 9 | - **Test UI**: `pnpm test:ui` (Playwright test runner with UI)
10 | - **Test headed**: `pnpm test:headed` (Run tests in visible browser)
11 | 
12 | ## Code Style
13 | - **Formatting**: Prettier with 2-space tabs, single quotes, no semicolons, 140 char line width
14 | - **Imports**: Use `.tsx`/`.ts` extensions in imports, group by external/internal
15 | - **Components**: React FC with explicit typing, PascalCase names
16 | - **Hooks**: Custom hooks start with `use`, camelCase
17 | - **Types**: Define interfaces in `src/types/index.ts`, use `type` for unions
18 | - **Files**: Use PascalCase for components, camelCase for hooks/utilities
19 | - **State**: Use proper TypeScript typing for all state variables
20 | - **Error handling**: Use try/catch blocks with proper error propagation
21 | - **Database**: IndexedDB with typed interfaces, async/await pattern
22 | - **Styling**: Tailwind CSS classes, responsive design patterns
23 | 
24 | ## Tech Stack
25 | React 19, TypeScript, Vite, Tailwind CSS, Hono API, Cloudflare Workers, IndexedDB, React Router, use-mcp
26 | 
27 | ## Chat UI Specific Guidelines
28 | 
29 | ### Background Animation
30 | - Use CSS `hue-rotate` filter for color cycling (more performant than gradient animation)
31 | - Isolate animations to `::before` pseudo-elements with `z-index: -1` to prevent inheritance
32 | - Use `background-size: 100% 100vh` and `background-repeat: repeat-y` for repeating patterns
33 | - Initialize with random animation delay using CSS custom properties
34 | 
35 | ### Modal Patterns
36 | - Use `rgba(0,0,0,0.8)` for modal backdrops (avoid gradients that cause banding)
37 | - Implement click-outside-to-dismiss with `onClick` on backdrop and `stopPropagation` on content
38 | - Add `cursor-pointer` to all clickable elements including close buttons
39 | - Use `document.body.style.overflow = 'hidden'` to prevent background scrolling
40 | 
41 | ### MCP Server Management
42 | - Store multiple servers in localStorage as JSON array (`mcpServers`)
43 | - Track tool counts separately (`mcpServerToolCounts`) for disabled servers
44 | - Use server-specific IDs for state management
45 | - Each MCP server gets unique `useMcp` hook instance for proper isolation
46 | - OAuth state parameter automatically handles multiple server differentiation
47 | 
48 | ### State Persistence
49 | - Use localStorage for user preferences (model selection, server configurations)
50 | - Use sessionStorage for temporary state (single server URL in legacy components)
51 | - Clear related data when removing servers or models
52 | 
53 | ### UI Indicators
54 | - Use emoji-based indicators (🧠 for models, 🔌 for MCP servers)
55 | - Format counts as `enabled/total` (e.g., "1/2 servers, 3/5 tools")
56 | - Place model selector on left, MCP servers on right for balance
57 | - Show "none" instead of red symbols for cleaner unconfigured states
58 | 
59 | ### Component Organization
60 | - Keep disabled components in React tree but hidden with CSS (`{false && ...}`)
61 | - This prevents MCP connections from being destroyed when modals close
62 | - Use conditional rendering sparingly, prefer CSS visibility for stateful components
63 | 
64 | ### Routing and OAuth
65 | - Use React Router for OAuth callback routes (`/oauth/callback`)
66 | - OAuth callback should match main app styling with loading indicators
67 | - use-mcp library handles multiple server OAuth flows automatically via state parameters
68 | 
69 | ### Performance Considerations
70 | - Avoid animating gradients directly (causes repainting)
71 | - Use transform and filter animations (hardware accelerated)
72 | - Aggregate tools from multiple sources efficiently
73 | - Minimize localStorage reads in render loops
74 | 


--------------------------------------------------------------------------------
/examples/chat-ui/MODEL_SELECTOR_GUIDE.md:
--------------------------------------------------------------------------------
 1 | # Model Selector Guide
 2 | 
 3 | This guide explains how to use the new favorites-based model selector system.
 4 | 
 5 | ## Overview
 6 | 
 7 | The chat UI now supports:
 8 | - **39 models** from 3 providers (Anthropic, Groq, OpenRouter)
 9 | - **Favorites system** - star your preferred models
10 | - **Search functionality** - find models by name or provider
11 | - **Tool filtering** - show only models that support tool calling
12 | - **OAuth authentication** - seamless OpenRouter integration
13 | 
14 | ## Features
15 | 
16 | ### Model Selection
17 | - Click the model selector to view all available models
18 | - Models are loaded from the live models.dev API
19 | - Each model shows provider logo, name, and capabilities
20 | 
21 | ### Favorites System
22 | - ⭐ Star models to add them to your favorites
23 | - Starred models are saved to local storage
24 | - Use the "Favorites" filter to show only starred models
25 | 
26 | ### Search & Filtering
27 | - 🔍 Search box to find models by name or provider
28 | - 🔧 "Tools Only" filter (appears when MCP tools are available)
29 | - ⭐ "Favorites" filter to show only starred models
30 | 
31 | ### Authentication
32 | - **API Keys**: Enter manually for Anthropic and Groq
33 | - **OAuth**: One-click authentication for OpenRouter
34 | - Authentication status shown with icons (✓ or ⚠️)
35 | 
36 | ## Provider Support
37 | 
38 | ### Anthropic
39 | - **Models**: 9 models including Claude 4 Sonnet
40 | - **Auth**: API key (requires manual entry)
41 | - **Tools**: All models support tool calling
42 | 
43 | ### Groq
44 | - **Models**: 13 models including Llama and Qwen variants
45 | - **Auth**: API key (requires manual entry)
46 | - **Tools**: 11 models support tool calling
47 | 
48 | ### OpenRouter
49 | - **Models**: 17 models from various providers
50 | - **Auth**: OAuth PKCE flow (one-click authentication)
51 | - **Tools**: All models support tool calling
52 | 
53 | ## Setting Up OpenRouter OAuth
54 | 
55 | 1. Create an OpenRouter account at https://openrouter.ai
56 | 2. Go to your dashboard and create an OAuth app
57 | 3. Set the redirect URI to: `http://localhost:5002/oauth/openrouter/callback`
58 | 4. Copy your client ID to your `.env` file:
59 |    ```
60 |    VITE_OPENROUTER_CLIENT_ID=your_client_id_here
61 |    ```
62 | 
63 | ## Usage Tips
64 | 
65 | 1. **Star your favorites** - This makes model selection much faster
66 | 2. **Use search** - With 39 models, search helps find what you need
67 | 3. **Filter by tools** - When using MCP tools, enable "Tools Only" filter
68 | 4. **Try different providers** - Each has unique models with different strengths
69 | 
70 | ## Model Data Updates
71 | 
72 | Model data is fetched from models.dev API and can be updated with:
73 | 
74 | ```bash
75 | pnpm update-models
76 | ```
77 | 
78 | This will refresh the model list with the latest information from the API.
79 | 
80 | ## Local Storage
81 | 
82 | The following preferences are saved locally:
83 | - **Favorites**: `aiChatTemplate_favorites_v1`
84 | - **Tokens**: `aiChatTemplate_token_[provider]`
85 | - **Selected Model**: `aiChatTemplate_selectedModel`
86 | 
87 | ## Troubleshooting
88 | 
89 | - **OAuth popup blocked**: Allow popups for this site
90 | - **Authentication failed**: Check your API keys or re-authenticate
91 | - **Model not working**: Verify the model supports the features you're using
92 | - **Tools not showing**: Enable "Tools Only" filter when MCP tools are configured
93 | 


--------------------------------------------------------------------------------
/examples/chat-ui/README.md:
--------------------------------------------------------------------------------
 1 | # AI Chat with MCP
 2 | 
 3 | A React-based AI chat application demonstrating [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) integration with multiple AI providers.
 4 | 
 5 | This static web application showcases how to use the [`use-mcp`](../../) library to connect to MCP servers, providing extensible AI capabilities through external tools and services. The app supports multiple AI models, stores conversations locally in IndexedDB, and includes OAuth authentication for MCP server connections.
 6 | 
 7 | **Live demo**: [chat.use-mcp.dev](https://chat.use-mcp.dev)
 8 | 
 9 | ## Features
10 | 
11 | - **MCP Integration**: Connect to MCP servers with OAuth authentication support
12 | - **Multi-model Support**: Anthropic (Claude) and Groq (Llama) models with API key authentication
13 | - **Local Storage**: Conversations stored in browser's IndexedDB
14 | - **Static Deployment**: Builds to static assets for deployment anywhere
15 | - **Modern Stack**: React 19, TypeScript, Tailwind CSS, Vite
16 | 
17 | ## Get started
18 | 
19 | ```sh
20 | pnpm install
21 | pnpm dev
22 | ```
23 | 
24 | Build and deploy:
25 | 
26 | ```sh
27 | pnpm build
28 | pnpm run deploy  # deploys to Cloudflare Pages
29 | ```
30 | 
31 | ## Development
32 | 
33 | - **Dev server**: `pnpm dev` (runs on port 5002)
34 | - **Build**: `pnpm build`
35 | - **Lint**: `pnpm lint`
36 | - **Test**: `pnpm test` (Playwright E2E tests)
37 | 


--------------------------------------------------------------------------------
/examples/chat-ui/api/index.ts:
--------------------------------------------------------------------------------
  1 | import type { LanguageModelV1StreamPart } from 'ai'
  2 | import { streamText, extractReasoningMiddleware, wrapLanguageModel } from 'ai'
  3 | import { createWorkersAI } from 'workers-ai-provider'
  4 | import { Hono } from 'hono'
  5 | 
  6 | interface Env {
  7 |   ASSETS: Fetcher
  8 |   AI: Ai
  9 | }
 10 | 
 11 | type message = {
 12 |   role: 'system' | 'user' | 'assistant' | 'data'
 13 |   content: string
 14 | }
 15 | 
 16 | const app = new Hono<{ Bindings: Env }>()
 17 | 
 18 | // Handle the /api/chat endpoint
 19 | app.post('/api/chat', async (c) => {
 20 |   try {
 21 |     const { messages, reasoning }: { messages: message[]; reasoning: boolean } = await c.req.json()
 22 | 
 23 |     const workersai = createWorkersAI({ binding: c.env.AI })
 24 | 
 25 |     // Choose model based on reasoning preference
 26 |     const model = reasoning
 27 |       ? wrapLanguageModel({
 28 |           model: workersai('@cf/deepseek-ai/deepseek-r1-distill-qwen-32b'),
 29 |           middleware: [
 30 |             extractReasoningMiddleware({ tagName: 'think' }),
 31 |             //custom middleware to inject <think> tag at the beginning of a reasoning if it is missing
 32 |             {
 33 |               wrapGenerate: async ({ doGenerate }) => {
 34 |                 const result = await doGenerate()
 35 | 
 36 |                 if (!result.text?.includes('<think>')) {
 37 |                   result.text = `<think>${result.text}`
 38 |                 }
 39 | 
 40 |                 return result
 41 |               },
 42 |               wrapStream: async ({ doStream }) => {
 43 |                 const { stream, ...rest } = await doStream()
 44 | 
 45 |                 let generatedText = ''
 46 |                 const transformStream = new TransformStream<LanguageModelV1StreamPart, LanguageModelV1StreamPart>({
 47 |                   transform(chunk, controller) {
 48 |                     //we are manually adding the <think> tag because some times, distills of reasoning models omit it
 49 |                     if (chunk.type === 'text-delta') {
 50 |                       if (!generatedText.includes('<think>')) {
 51 |                         generatedText += '<think>'
 52 |                         controller.enqueue({
 53 |                           type: 'text-delta',
 54 |                           textDelta: '<think>',
 55 |                         })
 56 |                       }
 57 |                       generatedText += chunk.textDelta
 58 |                     }
 59 | 
 60 |                     controller.enqueue(chunk)
 61 |                   },
 62 |                 })
 63 | 
 64 |                 return {
 65 |                   stream: stream.pipeThrough(transformStream),
 66 |                   ...rest,
 67 |                 }
 68 |               },
 69 |             },
 70 |           ],
 71 |         })
 72 |       : workersai('@cf/meta/llama-3.3-70b-instruct-fp8-fast')
 73 | 
 74 |     const systemPrompt: message = {
 75 |       role: 'system',
 76 |       content: `
 77 |         - Do not wrap your responses in html tags.
 78 |         - Do not apply any formatting to your responses.
 79 |         - You are an expert conversational chatbot. Your objective is to be as helpful as possible.
 80 |         - You must keep your responses relevant to the user's prompt.
 81 |         - You must respond with a maximum of 512 tokens (300 words). 
 82 |         - You must respond clearly and concisely, and explain your logic if required.
 83 |         - You must not provide any personal information.
 84 |         - Do not respond with your own personal opinions, and avoid topics unrelated to the user's prompt.
 85 |         ${
 86 |           messages.length <= 1 &&
 87 |           `- Important REMINDER: You MUST provide a 5 word title at the END of your response using <chat-title> </chat-title> tags. 
 88 |           If you do not do this, this session will error.
 89 |           For example, <chat-title>Hello and Welcome</chat-title> Hi, how can I help you today?
 90 |           `
 91 |         }
 92 |       `,
 93 |     }
 94 | 
 95 |     const text = await streamText({
 96 |       model,
 97 |       messages: [systemPrompt, ...messages],
 98 |       maxTokens: 2048,
 99 |       maxRetries: 3,
100 |     })
101 | 
102 |     return text.toDataStreamResponse({
103 |       sendReasoning: true,
104 |     })
105 |   } catch (error) {
106 |     return c.json({ error: `Chat completion failed. ${(error as Error)?.message}` }, 500)
107 |   }
108 | })
109 | 
110 | // Handle static assets and fallback routes
111 | app.all('*', async (c) => {
112 |   if (c.env.ASSETS) {
113 |     return c.env.ASSETS.fetch(c.req.raw)
114 |   }
115 |   return c.text('Not found', 404)
116 | })
117 | 
118 | export default app
119 | 


--------------------------------------------------------------------------------
/examples/chat-ui/e2e/build.spec.ts:
--------------------------------------------------------------------------------
 1 | import { test, expect } from '@playwright/test'
 2 | import { exec } from 'child_process'
 3 | import { promisify } from 'util'
 4 | 
 5 | const execAsync = promisify(exec)
 6 | 
 7 | interface ExecError extends Error {
 8 |   stdout?: string
 9 |   stderr?: string
10 | }
11 | 
12 | test.describe('Build Tests', () => {
13 |   test('should build without type errors', async () => {
14 |     try {
15 |       const { stderr } = await execAsync('pnpm build', {
16 |         cwd: process.cwd(),
17 |         timeout: 60000,
18 |       })
19 | 
20 |       // Check for TypeScript compilation errors
21 |       expect(stderr).not.toContain('error TS')
22 |       expect(stderr).not.toContain('Type error')
23 |     } catch (error) {
24 |       const execError = error as ExecError
25 |       console.error('Build failed:', execError.stdout, execError.stderr)
26 |       throw new Error(`Build failed: ${execError.message}`)
27 |     }
28 |   })
29 | 
30 |   test('should lint without errors', async () => {
31 |     try {
32 |       const { stderr } = await execAsync('pnpm lint', {
33 |         cwd: process.cwd(),
34 |         timeout: 30000,
35 |       })
36 | 
37 |       // ESLint should pass without errors
38 |       expect(stderr).not.toContain('error')
39 |     } catch (error) {
40 |       const execError = error as ExecError
41 |       console.error('Lint failed:', execError.stdout, execError.stderr)
42 |       throw new Error(`Lint failed: ${execError.message}`)
43 |     }
44 |   })
45 | })
46 | 


--------------------------------------------------------------------------------
/examples/chat-ui/e2e/chat.spec.ts:
--------------------------------------------------------------------------------
  1 | import { test, expect } from '@playwright/test'
  2 | 
  3 | test.describe('Chat Functionality', () => {
  4 |   test.beforeEach(async ({ page }) => {
  5 |     // Mock the external AI API calls
  6 |     await page.route('https://api.groq.com/**', async (route) => {
  7 |       // Mock streaming response for Groq
  8 |       const mockResponse = 'Hello! This is a mocked response from the AI assistant.'
  9 | 
 10 |       route.fulfill({
 11 |         status: 200,
 12 |         headers: {
 13 |           'Content-Type': 'text/plain',
 14 |         },
 15 |         body: mockResponse,
 16 |       })
 17 |     })
 18 | 
 19 |     await page.route('https://api.anthropic.com/**', async (route) => {
 20 |       // Mock streaming response for Anthropic
 21 |       const mockResponse = 'Hello! This is a mocked response from Claude.'
 22 | 
 23 |       route.fulfill({
 24 |         status: 200,
 25 |         headers: {
 26 |           'Content-Type': 'text/plain',
 27 |         },
 28 |         body: mockResponse,
 29 |       })
 30 |     })
 31 | 
 32 |     // Mock the local API endpoint
 33 |     await page.route('/api/chat', async (route) => {
 34 |       const mockResponse = {
 35 |         choices: [
 36 |           {
 37 |             delta: {
 38 |               content: 'Hello! This is a mocked response from the local API.',
 39 |             },
 40 |           },
 41 |         ],
 42 |       }
 43 | 
 44 |       route.fulfill({
 45 |         status: 200,
 46 |         headers: {
 47 |           'Content-Type': 'application/json',
 48 |         },
 49 |         body: JSON.stringify(mockResponse),
 50 |       })
 51 |     })
 52 |   })
 53 | 
 54 |   test('should load the chat interface', async ({ page }) => {
 55 |     await page.goto('/')
 56 | 
 57 |     // Check if the main elements are present
 58 |     await expect(page.locator('body')).toBeVisible()
 59 | 
 60 |     // Look for chat-related elements (adjust selectors based on your UI)
 61 |     const messageInput = page.locator('textarea, input[type="text"]').first()
 62 |     await expect(messageInput).toBeVisible()
 63 | 
 64 |     // Check for a send button or similar
 65 |     const sendButton = page
 66 |       .locator('button')
 67 |       .filter({ hasText: /send|submit/i })
 68 |       .first()
 69 |     if (await sendButton.isVisible()) {
 70 |       await expect(sendButton).toBeVisible()
 71 |     }
 72 |   })
 73 | 
 74 |   test('should send a message and receive a response', async ({ page }) => {
 75 |     await page.goto('/')
 76 | 
 77 |     // Wait for the page to load
 78 |     await page.waitForLoadState('networkidle')
 79 | 
 80 |     // Find the message input field
 81 |     const messageInput = page.locator('textarea, input[type="text"]').first()
 82 |     await expect(messageInput).toBeVisible()
 83 | 
 84 |     // Type a test message
 85 |     const testMessage = 'Hello, can you help me?'
 86 |     await messageInput.fill(testMessage)
 87 |     await expect(messageInput).toHaveValue(testMessage)
 88 | 
 89 |     // Send the message by pressing Enter (most common method)
 90 |     await messageInput.press('Enter')
 91 | 
 92 |     // Wait a moment for the form submission to process
 93 |     await page.waitForTimeout(1000)
 94 | 
 95 |     // Check that a conversation has started - look for any text content that might be a message
 96 |     const pageContent = await page.textContent('body')
 97 |     expect(pageContent).toContain(testMessage)
 98 | 
 99 |     // The input may or may not clear depending on implementation details
100 |     // This is less critical than the message appearing
101 |   })
102 | 
103 |   test('should handle API errors gracefully', async ({ page }) => {
104 |     // Override mocks to simulate API errors
105 |     await page.route('https://api.groq.com/**', async (route) => {
106 |       route.fulfill({
107 |         status: 500,
108 |         headers: {
109 |           'Content-Type': 'application/json',
110 |         },
111 |         body: JSON.stringify({ error: 'Internal server error' }),
112 |       })
113 |     })
114 | 
115 |     await page.route('https://api.anthropic.com/**', async (route) => {
116 |       route.fulfill({
117 |         status: 401,
118 |         headers: {
119 |           'Content-Type': 'application/json',
120 |         },
121 |         body: JSON.stringify({ error: 'Unauthorized' }),
122 |       })
123 |     })
124 | 
125 |     await page.goto('/')
126 | 
127 |     // Try to send a message
128 |     const messageInput = page.locator('textarea, input[type="text"]').first()
129 |     await messageInput.fill('Test message')
130 | 
131 |     const sendButton = page
132 |       .locator('button')
133 |       .filter({ hasText: /send|submit/i })
134 |       .first()
135 |     if (await sendButton.isVisible()) {
136 |       await sendButton.click()
137 |     } else {
138 |       await messageInput.press('Enter')
139 |     }
140 | 
141 |     // Should handle the error without crashing
142 |     await page.waitForTimeout(2000)
143 | 
144 |     // The app should still be functional (not showing a white screen or error page)
145 |     await expect(page.locator('body')).toBeVisible()
146 |   })
147 | 
148 |   test('should persist conversations', async ({ page }) => {
149 |     await page.goto('/')
150 | 
151 |     // Send a message
152 |     const messageInput = page.locator('textarea, input[type="text"]').first()
153 |     await messageInput.fill('Test message for persistence')
154 | 
155 |     const sendButton = page
156 |       .locator('button')
157 |       .filter({ hasText: /send|submit/i })
158 |       .first()
159 |     if (await sendButton.isVisible()) {
160 |       await sendButton.click()
161 |     } else {
162 |       await messageInput.press('Enter')
163 |     }
164 | 
165 |     await page.waitForTimeout(2000)
166 | 
167 |     // Refresh the page
168 |     await page.reload()
169 |     await page.waitForLoadState('networkidle')
170 | 
171 |     // Check if the conversation persisted (this depends on your IndexedDB implementation)
172 |     // We'll just verify the page loads correctly after refresh
173 |     await expect(page.locator('body')).toBeVisible()
174 | 
175 |     const inputAfterReload = page.locator('textarea, input[type="text"]').first()
176 |     await expect(inputAfterReload).toBeVisible()
177 |   })
178 | })
179 | 


--------------------------------------------------------------------------------
/examples/chat-ui/eslint.config.js:
--------------------------------------------------------------------------------
 1 | import js from '@eslint/js'
 2 | import globals from 'globals'
 3 | import reactHooks from 'eslint-plugin-react-hooks'
 4 | import reactRefresh from 'eslint-plugin-react-refresh'
 5 | import tseslint from 'typescript-eslint'
 6 | 
 7 | export default tseslint.config(
 8 |   { ignores: ['dist'] },
 9 |   {
10 |     extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 |     files: ['**/*.{ts,tsx}'],
12 |     languageOptions: {
13 |       ecmaVersion: 2020,
14 |       globals: globals.browser,
15 |     },
16 |     plugins: {
17 |       'react-hooks': reactHooks,
18 |       'react-refresh': reactRefresh,
19 |     },
20 |     rules: {
21 |       ...reactHooks.configs.recommended.rules,
22 |       'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
23 |     },
24 |   }
25 | )
26 | 


--------------------------------------------------------------------------------
/examples/chat-ui/index.html:
--------------------------------------------------------------------------------
 1 | <!doctype html>
 2 | <html lang="en">
 3 |   <head>
 4 |     <meta charset="UTF-8" />
 5 |     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
 6 |     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 7 |     <title>use-mcp chat</title>
 8 |   </head>
 9 |   <body>
10 |     <div id="root"></div>
11 |     <script type="module" src="/src/main.tsx"></script>
12 |   </body>
13 | </html>
14 | 


--------------------------------------------------------------------------------
/examples/chat-ui/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "ai-chat-template",
 3 |   "private": true,
 4 |   "version": "0.0.0",
 5 |   "type": "module",
 6 |   "scripts": {
 7 |     "dev": "vite --port=5002",
 8 |     "build": "tsc -b && vite build",
 9 |     "lint": "eslint .",
10 |     "preview": "vite preview",
11 |     "deploy": "npm run build && npm exec wrangler deploy",
12 |     "test": "playwright test",
13 |     "test:ui": "playwright test --ui",
14 |     "test:headed": "playwright test --headed",
15 |     "test:html": "playwright test --reporter=html",
16 |     "update-models": "tsx scripts/update-models.ts"
17 |   },
18 |   "dependencies": {
19 |     "@ai-sdk/anthropic": "^1.2.12",
20 |     "@ai-sdk/groq": "^1.2.9",
21 |     "@ai-sdk/openai": "^1.3.23",
22 |     "@tailwindcss/typography": "^0.5.16",
23 |     "@tailwindcss/vite": "^4.0.14",
24 |     "ai": "^4.1.61",
25 |     "hono": "^4.7.9",
26 |     "idb": "^8.0.2",
27 |     "lucide-react": "^0.482.0",
28 |     "react": "^19.0.0",
29 |     "react-dom": "^19.0.0",
30 |     "react-markdown": "^10.1.0",
31 |     "react-router-dom": "^7.6.2",
32 |     "rehype-highlight": "^7.0.2",
33 |     "remark-gfm": "^4.0.1",
34 |     "tailwindcss": "^4.0.14",
35 |     "workers-ai-provider": "^0.1.3"
36 |   },
37 |   "devDependencies": {
38 |     "@ai-sdk/ui-utils": "^1.2.11",
39 |     "@cloudflare/vite-plugin": "^0.1.12",
40 |     "@cloudflare/workers-types": "^4.20250317.0",
41 |     "@eslint/js": "^9.21.0",
42 |     "@playwright/test": "^1.52.0",
43 |     "@types/react": "^19.0.10",
44 |     "@types/react-dom": "^19.0.4",
45 |     "@vitejs/plugin-react": "^4.3.4",
46 |     "eslint": "^9.21.0",
47 |     "eslint-plugin-react-hooks": "^5.1.0",
48 |     "eslint-plugin-react-refresh": "^0.4.19",
49 |     "globals": "^15.15.0",
50 |     "prettier": "^3.5.3",
51 |     "tsx": "^4.20.3",
52 |     "typescript": "~5.7.2",
53 |     "typescript-eslint": "^8.24.1",
54 |     "use-mcp": "link:../..",
55 |     "vite": "^6.2.0",
56 |     "wrangler": "^4.1.0"
57 |   },
58 |   "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
59 | }
60 | 


--------------------------------------------------------------------------------
/examples/chat-ui/playwright.config.ts:
--------------------------------------------------------------------------------
 1 | import { defineConfig, devices } from '@playwright/test'
 2 | 
 3 | /**
 4 |  * @see https://playwright.dev/docs/test-configuration
 5 |  */
 6 | export default defineConfig({
 7 |   testDir: './e2e',
 8 |   /* Run tests in files in parallel */
 9 |   fullyParallel: true,
10 |   /* Fail the build on CI if you accidentally left test.only in the source code. */
11 |   forbidOnly: !!process.env.CI,
12 |   /* Retry on CI only */
13 |   retries: process.env.CI ? 2 : 0,
14 |   /* Opt out of parallel tests on CI. */
15 |   workers: process.env.CI ? 1 : undefined,
16 |   /* Reporter to use. See https://playwright.dev/docs/test-reporters */
17 |   reporter: 'list',
18 |   /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
19 |   use: {
20 |     /* Base URL to use in actions like `await page.goto('/')`. */
21 |     baseURL: 'http://localhost:5173',
22 | 
23 |     /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
24 |     trace: 'on-first-retry',
25 |   },
26 | 
27 |   /* Configure projects for major browsers */
28 |   projects: [
29 |     {
30 |       name: 'chromium',
31 |       use: { ...devices['Desktop Chrome'] },
32 |     },
33 |   ],
34 | 
35 |   /* Run your local dev server before starting the tests */
36 |   webServer: {
37 |     command: 'pnpm dev',
38 |     url: 'http://localhost:5173',
39 |     reuseExistingServer: !process.env.CI,
40 |   },
41 | })
42 | 


--------------------------------------------------------------------------------
/examples/chat-ui/public/vite.svg:
--------------------------------------------------------------------------------
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>


--------------------------------------------------------------------------------
/examples/chat-ui/scripts/update-models.ts:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env tsx
  2 | 
  3 | import { writeFileSync } from 'fs'
  4 | import { join } from 'path'
  5 | 
  6 | interface ModelData {
  7 |   id: string
  8 |   name: string
  9 |   attachment: boolean
 10 |   reasoning: boolean
 11 |   temperature: boolean
 12 |   tool_call: boolean
 13 |   knowledge: string
 14 |   release_date: string
 15 |   last_updated: string
 16 |   modalities: {
 17 |     input: string[]
 18 |     output: string[]
 19 |   }
 20 |   open_weights: boolean
 21 |   limit: {
 22 |     context: number
 23 |     output: number
 24 |   }
 25 |   cost?: {
 26 |     input: number
 27 |     output: number
 28 |     cache_read?: number
 29 |     cache_write?: number
 30 |   }
 31 | }
 32 | 
 33 | interface ProviderData {
 34 |   models: Record<string, ModelData>
 35 | }
 36 | 
 37 | interface ModelsDevData {
 38 |   anthropic: ProviderData
 39 |   groq: ProviderData
 40 |   openrouter: ProviderData
 41 |   [key: string]: ProviderData
 42 | }
 43 | 
 44 | const SUPPORTED_PROVIDERS = ['anthropic', 'groq', 'openrouter'] as const
 45 | type SupportedProvider = (typeof SUPPORTED_PROVIDERS)[number]
 46 | 
 47 | async function fetchModelsData(): Promise<ModelsDevData> {
 48 |   console.log('Fetching models data from models.dev...')
 49 | 
 50 |   const response = await fetch('https://models.dev/api.json')
 51 |   if (!response.ok) {
 52 |     throw new Error(`Failed to fetch models data: ${response.status} ${response.statusText}`)
 53 |   }
 54 | 
 55 |   return await response.json()
 56 | }
 57 | 
 58 | function filterAndTransformModels(data: ModelsDevData) {
 59 |   const filtered: Record<SupportedProvider, Record<string, ModelData>> = {
 60 |     anthropic: {},
 61 |     groq: {},
 62 |     openrouter: {},
 63 |   }
 64 | 
 65 |   // Filter by supported providers
 66 |   for (const provider of SUPPORTED_PROVIDERS) {
 67 |     if (data[provider] && data[provider].models) {
 68 |       filtered[provider] = data[provider].models
 69 |     }
 70 |   }
 71 | 
 72 |   return filtered
 73 | }
 74 | 
 75 | async function main() {
 76 |   try {
 77 |     const data = await fetchModelsData()
 78 |     const filteredData = filterAndTransformModels(data)
 79 | 
 80 |     const outputPath = join(process.cwd(), 'src', 'data', 'models.json')
 81 |     writeFileSync(outputPath, JSON.stringify(filteredData, null, 2))
 82 | 
 83 |     console.log(`✅ Models data updated successfully at ${outputPath}`)
 84 | 
 85 |     // Print summary
 86 |     let totalModels = 0
 87 |     let toolSupportingModels = 0
 88 | 
 89 |     for (const [provider, models] of Object.entries(filteredData)) {
 90 |       const modelCount = Object.keys(models).length
 91 |       const toolModels = Object.values(models).filter((m) => m.tool_call).length
 92 | 
 93 |       console.log(`  ${provider}: ${modelCount} models (${toolModels} support tools)`)
 94 |       totalModels += modelCount
 95 |       toolSupportingModels += toolModels
 96 |     }
 97 | 
 98 |     console.log(`\nTotal: ${totalModels} models (${toolSupportingModels} support tools)`)
 99 |   } catch (error) {
100 |     console.error('❌ Failed to update models data:', error)
101 |     process.exit(1)
102 |   }
103 | }
104 | 
105 | main()
106 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/App.tsx:
--------------------------------------------------------------------------------
 1 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
 2 | import ChatApp from './components/ChatApp'
 3 | import OAuthCallback from './components/OAuthCallback'
 4 | 
 5 | function App() {
 6 |   return (
 7 |     <Router>
 8 |       <Routes>
 9 |         <Route path="/oauth/openrouter/callback" element={<OAuthCallback provider="openrouter" />} />
10 |         <Route path="/oauth/callback" element={<OAuthCallback provider="openrouter" />} />
11 |         <Route path="/" element={<ChatApp />} />
12 |       </Routes>
13 |     </Router>
14 |   )
15 | }
16 | 
17 | export default App
18 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/components/ApiKeyModal.tsx:
--------------------------------------------------------------------------------
  1 | import React, { useState } from 'react'
  2 | import { createPortal } from 'react-dom'
  3 | import { X, Eye, EyeOff } from 'lucide-react'
  4 | import { type Provider } from '../types/models'
  5 | 
  6 | interface ApiKeyModalProps {
  7 |   isOpen: boolean
  8 |   onClose: () => void
  9 |   provider: Provider
 10 |   onSave: (apiKey: string) => void
 11 | }
 12 | 
 13 | const ApiKeyModal: React.FC<ApiKeyModalProps> = ({ isOpen, onClose, provider, onSave }) => {
 14 |   const [apiKey, setApiKey] = useState('')
 15 |   const [showApiKey, setShowApiKey] = useState(false)
 16 | 
 17 |   if (!isOpen) return null
 18 | 
 19 |   const handleSubmit = (e: React.FormEvent) => {
 20 |     e.preventDefault()
 21 |     if (apiKey.trim()) {
 22 |       onSave(apiKey.trim())
 23 |       setApiKey('')
 24 |       onClose()
 25 |     }
 26 |   }
 27 | 
 28 |   const handleClose = () => {
 29 |     setApiKey('')
 30 |     onClose()
 31 |   }
 32 | 
 33 |   const modalContent = (
 34 |     <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" onClick={handleClose}>
 35 |       <div className="bg-white rounded-lg p-6 w-full max-w-md mx-4" onClick={(e) => e.stopPropagation()}>
 36 |         <div className="flex items-center justify-between mb-4">
 37 |           <h2 className="text-lg font-semibold text-zinc-900">
 38 |             {provider.logo} {provider.name} API Key
 39 |           </h2>
 40 |           <button onClick={handleClose} className="text-zinc-400 hover:text-zinc-600 p-1">
 41 |             <X size={20} />
 42 |           </button>
 43 |         </div>
 44 | 
 45 |         <p className="text-sm text-zinc-600 mb-4">
 46 |           Enter your {provider.name} API key to use this model. Your key will be stored locally in your browser.
 47 |         </p>
 48 | 
 49 |         <form onSubmit={handleSubmit}>
 50 |           <div className="mb-4">
 51 |             <label htmlFor="apiKey" className="block text-sm font-medium text-zinc-700 mb-2">
 52 |               API Key
 53 |             </label>
 54 |             <div className="relative">
 55 |               <input
 56 |                 type={showApiKey ? 'text' : 'password'}
 57 |                 id="apiKey"
 58 |                 value={apiKey}
 59 |                 onChange={(e) => setApiKey(e.target.value)}
 60 |                 className="w-full px-3 py-2 border border-zinc-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent pr-10"
 61 |                 placeholder={`Enter your ${provider.name} API key`}
 62 |                 autoComplete="off"
 63 |               />
 64 |               <button
 65 |                 type="button"
 66 |                 onClick={() => setShowApiKey(!showApiKey)}
 67 |                 className="absolute inset-y-0 right-0 pr-3 flex items-center text-zinc-400 hover:text-zinc-600"
 68 |               >
 69 |                 {showApiKey ? <EyeOff size={16} /> : <Eye size={16} />}
 70 |               </button>
 71 |             </div>
 72 |           </div>
 73 | 
 74 |           <div className="flex items-center justify-between">
 75 |             <a
 76 |               href={provider.documentationUrl}
 77 |               target="_blank"
 78 |               rel="noopener noreferrer"
 79 |               className="text-sm text-blue-600 hover:text-blue-800"
 80 |             >
 81 |               Get API key →
 82 |             </a>
 83 |             <div className="flex gap-2">
 84 |               <button type="button" onClick={handleClose} className="px-4 py-2 text-sm text-zinc-600 hover:text-zinc-800">
 85 |                 Cancel
 86 |               </button>
 87 |               <button
 88 |                 type="submit"
 89 |                 disabled={!apiKey.trim()}
 90 |                 className="px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-zinc-300 disabled:cursor-not-allowed"
 91 |               >
 92 |                 Save
 93 |               </button>
 94 |             </div>
 95 |           </div>
 96 |         </form>
 97 |       </div>
 98 |     </div>
 99 |   )
100 | 
101 |   return createPortal(modalContent, document.body)
102 | }
103 | 
104 | export default ApiKeyModal
105 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/components/ChatApp.tsx:
--------------------------------------------------------------------------------
  1 | import React, { useState, useEffect } from 'react'
  2 | import ConversationThread from './ConversationThread.tsx'
  3 | import ChatSidebar from './ChatSidebar'
  4 | import ChatNavbar from './ChatNavbar'
  5 | import { storeName } from '../consts.ts'
  6 | import { type Conversation } from '../types'
  7 | import { useIndexedDB } from '../hooks/useIndexedDB'
  8 | import { type Model } from '../types/models'
  9 | import { getSelectedModel, setSelectedModel as saveSelectedModel } from '../utils/modelPreferences'
 10 | import { useModels } from '../hooks/useModels'
 11 | import { type IDBPDatabase } from 'idb'
 12 | import { type Tool } from 'use-mcp/react'
 13 | 
 14 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type
 15 | interface ChatAppProps {}
 16 | 
 17 | const ChatApp: React.FC<ChatAppProps> = () => {
 18 |   const [conversations, setConversations] = useState<Conversation[]>([])
 19 |   const [conversationId, setConversationId] = useState<number | undefined>(undefined)
 20 |   const [sidebarVisible, setSidebarVisible] = useState(false)
 21 | 
 22 |   const [selectedModel, setSelectedModel] = useState<Model>(getSelectedModel())
 23 |   const [apiKeyUpdateTrigger, setApiKeyUpdateTrigger] = useState<number>(0)
 24 |   const [mcpTools, setMcpTools] = useState<Tool[]>([])
 25 |   const [animationDelay] = useState<number>(() => -Math.random() * 60)
 26 |   const db = useIndexedDB()
 27 |   const { models, addToFavorites, toggleFavorite, isFavorite, getFavoriteModels } = useModels()
 28 | 
 29 |   const handleApiKeyUpdate = () => {
 30 |     setApiKeyUpdateTrigger((prev) => prev + 1)
 31 |   }
 32 | 
 33 |   // Handle OAuth success messages from popups
 34 |   useEffect(() => {
 35 |     const handleMessage = (event: MessageEvent) => {
 36 |       if (event.data.type === 'oauth_success') {
 37 |         handleApiKeyUpdate()
 38 |       }
 39 |     }
 40 | 
 41 |     window.addEventListener('message', handleMessage)
 42 |     return () => window.removeEventListener('message', handleMessage)
 43 |   }, [])
 44 | 
 45 |   const handleModelChange = (model: Model) => {
 46 |     setSelectedModel(model)
 47 |     saveSelectedModel(model)
 48 |   }
 49 | 
 50 |   // set up conversations on app load
 51 |   useEffect(() => {
 52 |     getConversations()
 53 |     deleteUnusedConversations()
 54 |     startNewConversation()
 55 |   }, [db])
 56 | 
 57 |   // Initialize sidebar visibility based on screen size
 58 |   useEffect(() => {
 59 |     // const isMobile = window.matchMedia("(max-width: 768px)").matches;
 60 |     // setSidebarVisible(!isMobile);
 61 |   }, [])
 62 | 
 63 |   const getConversations = async () => {
 64 |     if (!db) return
 65 | 
 66 |     const conversations = (await db.getAll(storeName)) as Conversation[]
 67 |     const inverseConversations = conversations.reverse()
 68 |     setConversations(inverseConversations)
 69 |   }
 70 | 
 71 |   const deleteConversation = async (id: number, showPromptToUser = true) => {
 72 |     try {
 73 |       if (showPromptToUser && !window.confirm('Are you sure you want to delete this conversation?')) {
 74 |         return
 75 |       }
 76 | 
 77 |       await db?.delete(storeName, id)
 78 |       setConversations((prev) => prev.filter((conv) => conv.id !== id))
 79 |       setConversationId(conversations[0]?.id)
 80 |     } catch (error) {
 81 |       console.error('Failed to delete conversation:', error)
 82 |     }
 83 |   }
 84 | 
 85 |   const editConversationTitle = async (id: number, newTitle: string) => {
 86 |     const conversation = (await db!.get(storeName, id)) as Conversation
 87 |     conversation.title = newTitle
 88 |     await db!.put(storeName, conversation)
 89 |     setConversations((prev) => prev.map((conv) => (conv.id === id ? { ...conv, title: newTitle } : conv)))
 90 |   }
 91 | 
 92 |   const startNewConversation = async () => {
 93 |     //create unique id for new conversation
 94 |     setConversationId(Date.now() + Math.floor(Math.random() * 1000))
 95 |     // if (window.matchMedia("(max-width: 768px)").matches) {
 96 |     //   setSidebarVisible(false);
 97 |     // }
 98 |   }
 99 | 
100 |   // delete conversations with no messages
101 |   const deleteUnusedConversations = async () => {
102 |     if (!db) return
103 |     const conversations = (await db.getAll(storeName)) as Conversation[]
104 |     const unusedConversations = conversations.filter((conversation) => conversation.messages.length === 0)
105 | 
106 |     for (const conversation of unusedConversations) {
107 |       deleteConversation(conversation.id as number, false)
108 |     }
109 |   }
110 | 
111 |   return (
112 |     <div
113 |       className="flex min-h-screen w-screen animated-bg-container"
114 |       style={{ '--random-delay': `${animationDelay}s` } as React.CSSProperties}
115 |     >
116 |       <div className="flex flex-row flex-grow flex-1 min-h-screen relative">
117 |         {/* Sidebar and Navbar components */}
118 |         {false && (
119 |           <>
120 |             <ChatSidebar
121 |               sidebarVisible={sidebarVisible}
122 |               setSidebarVisible={setSidebarVisible}
123 |               conversations={conversations}
124 |               conversationId={conversationId}
125 |               setConversationId={setConversationId}
126 |               deleteConversation={deleteConversation}
127 |               editConversationTitle={editConversationTitle}
128 |               startNewConversation={startNewConversation}
129 |               selectedModel={selectedModel}
130 |               onModelChange={handleModelChange}
131 |               apiKeyUpdateTrigger={apiKeyUpdateTrigger}
132 |               onMcpToolsUpdate={setMcpTools}
133 |               mcpTools={mcpTools}
134 |             />
135 |             <ChatNavbar sidebarVisible={sidebarVisible} setSidebarVisible={setSidebarVisible} />
136 |           </>
137 |         )}
138 |         <div className="flex flex-col flex-grow h-full w-full">
139 |           <ConversationThread
140 |             conversations={conversations}
141 |             conversationId={conversationId}
142 |             setConversationId={setConversationId}
143 |             setConversations={setConversations}
144 |             db={db as IDBPDatabase}
145 |             selectedModel={selectedModel}
146 |             onApiKeyUpdate={handleApiKeyUpdate}
147 |             onModelChange={handleModelChange}
148 |             apiKeyUpdateTrigger={apiKeyUpdateTrigger}
149 |             mcpTools={mcpTools}
150 |             onMcpToolsUpdate={setMcpTools}
151 |             addToFavorites={addToFavorites}
152 |             models={models}
153 |             toggleFavorite={toggleFavorite}
154 |             isFavorite={isFavorite}
155 |             getFavoriteModels={getFavoriteModels}
156 |           />
157 |         </div>
158 |       </div>
159 |     </div>
160 |   )
161 | }
162 | 
163 | export default ChatApp
164 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/components/ChatInput.tsx:
--------------------------------------------------------------------------------
 1 | import React, { FormEvent, KeyboardEvent, useRef, useEffect, Dispatch, SetStateAction } from 'react'
 2 | import { Send, Pause } from 'lucide-react'
 3 | 
 4 | interface ChatInputProps {
 5 |   input: string
 6 |   setInput: Dispatch<SetStateAction<string>>
 7 |   handleSubmit: (e: FormEvent) => void
 8 |   isLoading: boolean
 9 |   streamStarted: boolean
10 |   controller: AbortController
11 |   messagesCount: number
12 | }
13 | 
14 | const ChatInput: React.FC<ChatInputProps> = ({ input, setInput, handleSubmit, isLoading, streamStarted, controller, messagesCount }) => {
15 |   const textareaRef = useRef<HTMLTextAreaElement>(null)
16 | 
17 |   useEffect(() => {
18 |     if (textareaRef.current) {
19 |       textareaRef.current.style.height = 'auto'
20 |       textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`
21 |     }
22 |   }, [input])
23 | 
24 |   const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
25 |     if (e.key === 'Enter' && !e.shiftKey) {
26 |       e.preventDefault()
27 |       handleSubmit(e as unknown as FormEvent)
28 |     }
29 |     if (e.key === 'Enter' && e.shiftKey) {
30 |       e.preventDefault()
31 |       setInput((prev: string) => prev + '\n')
32 |     }
33 |   }
34 | 
35 |   return (
36 |     <div className="relative rounded-lg border border-zinc-200 bg-white shadow-sm">
37 |       <form onSubmit={handleSubmit} className={`w-full items-center`}>
38 |         <div className="flex justify-end items-center">
39 |           <textarea
40 |             id="chat-input"
41 |             ref={textareaRef}
42 |             value={input}
43 |             rows={1}
44 |             onFocus={() => console.clear()}
45 |             onChange={(e) => setInput(e.target.value)}
46 |             onKeyDown={handleKeyDown}
47 |             className="w-full flex-1 p-3 bg-transparent resize-none focus:outline-none"
48 |             placeholder={messagesCount === 0 ? 'Ask anything...' : 'Type your message...'}
49 |             style={{ maxHeight: '200px' }}
50 |           />
51 |           <button
52 |             type={isLoading && streamStarted ? 'button' : 'submit'}
53 |             onClick={isLoading && streamStarted ? () => controller.abort() : undefined}
54 |             className="m-2 hover:bg-zinc-100 rounded-lg p-2 text-zinc-600 hover:text-zinc-900"
55 |             disabled={!isLoading && !input.trim()}
56 |           >
57 |             {isLoading && streamStarted ? <Pause size={20} /> : <Send size={20} />}
58 |           </button>
59 |         </div>
60 |       </form>
61 |     </div>
62 |   )
63 | }
64 | 
65 | export default ChatInput
66 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/components/ChatNavbar.tsx:
--------------------------------------------------------------------------------
 1 | import React from 'react'
 2 | import { PanelLeftOpen } from 'lucide-react'
 3 | 
 4 | interface ChatNavbarProps {
 5 |   sidebarVisible: boolean
 6 |   setSidebarVisible: React.Dispatch<React.SetStateAction<boolean>>
 7 | }
 8 | 
 9 | const ChatNavbar: React.FC<ChatNavbarProps> = ({ sidebarVisible, setSidebarVisible }) => {
10 |   return (
11 |     <div className="sticky top-0 bg-white border-b border-zinc-200 z-10">
12 |       <div className="my-3 mx-2 flex items-center justify-between">
13 |         <div className="flex items-center">
14 |           {!sidebarVisible && (
15 |             <div>
16 |               <button
17 |                 onClick={() => setSidebarVisible(!sidebarVisible)}
18 |                 className="
19 |                 rounded-lg p-[0.4em]
20 |                 hover:bg-zinc-100 hover:cursor-pointer
21 |                 mr-2 transition-colors text-zinc-600 hover:text-zinc-800"
22 |               >
23 |                 <PanelLeftOpen size={20} />
24 |               </button>
25 |             </div>
26 |           )}
27 |           <h1 className="text-base font-semibold text-zinc-600 ml-2">AI Chat Template</h1>
28 |         </div>
29 |       </div>
30 |     </div>
31 |   )
32 | }
33 | 
34 | export default ChatNavbar
35 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/components/ChatSidebar.tsx:
--------------------------------------------------------------------------------
  1 | import React from 'react'
  2 | import { Edit, Trash2, PanelLeftClose, PanelLeftOpen, SquarePen } from 'lucide-react'
  3 | import ModelSelector from './ModelSelector'
  4 | import { McpServers } from './McpServers'
  5 | import { type Model } from '../types/models'
  6 | import { type Message } from '../types'
  7 | import { type Tool } from 'use-mcp/react'
  8 | 
  9 | interface Conversation {
 10 |   id?: number
 11 |   title: string
 12 |   messages: Message[]
 13 | }
 14 | 
 15 | interface ChatSidebarProps {
 16 |   sidebarVisible: boolean
 17 |   setSidebarVisible: React.Dispatch<React.SetStateAction<boolean>>
 18 |   conversations: Conversation[]
 19 |   conversationId: number | undefined
 20 |   setConversationId: React.Dispatch<React.SetStateAction<number | undefined>>
 21 |   deleteConversation: (id: number) => void
 22 |   editConversationTitle: (id: number, newTitle: string) => void
 23 |   startNewConversation: () => void
 24 |   selectedModel: Model
 25 |   onModelChange: (model: Model) => void
 26 |   apiKeyUpdateTrigger: number
 27 |   onMcpToolsUpdate: (tools: Tool[]) => void
 28 |   mcpTools: Tool[]
 29 | }
 30 | 
 31 | const ChatSidebar: React.FC<ChatSidebarProps> = ({
 32 |   sidebarVisible,
 33 |   setSidebarVisible,
 34 |   conversations,
 35 |   conversationId,
 36 |   setConversationId,
 37 |   deleteConversation,
 38 |   editConversationTitle,
 39 |   startNewConversation,
 40 |   selectedModel,
 41 |   onModelChange,
 42 |   apiKeyUpdateTrigger,
 43 |   onMcpToolsUpdate,
 44 |   mcpTools,
 45 | }) => {
 46 |   const handleConversationClick = (id: number | undefined) => {
 47 |     setConversationId(id)
 48 |     if (window.matchMedia('(max-width: 768px)').matches) {
 49 |       setSidebarVisible(false)
 50 |     }
 51 |   }
 52 | 
 53 |   return (
 54 |     <>
 55 |       {/* Mobile overlay */}
 56 |       {sidebarVisible && <div className="md:hidden fixed inset-0 bg-black/30 z-20" onClick={() => setSidebarVisible(false)} />}
 57 |       <div
 58 |         className={`
 59 |           fixed md:relative
 60 |           z-30 md:z-auto
 61 |           h-full
 62 |           bg-zinc-50
 63 |           transition-all duration-300
 64 |           
 65 |           ${sidebarVisible ? 'w-64 lg:w-[32rem] translate-x-0' : 'w-0 -translate-x-full md:translate-x-0'}
 66 |         `}
 67 |       >
 68 |         <div
 69 |           className={`m-2 flex items-center justify-between 
 70 |           ${sidebarVisible ? 'sticky' : 'hidden'}
 71 |           `}
 72 |         >
 73 |           <button
 74 |             onClick={() => setSidebarVisible(!sidebarVisible)}
 75 |             className="
 76 |               rounded-lg p-[0.4em]
 77 |               hover:bg-zinc-100 hover:cursor-pointer
 78 |               transition-colors text-zinc-600 hover:text-zinc-800"
 79 |           >
 80 |             {sidebarVisible ? <PanelLeftClose size={20} /> : <PanelLeftOpen size={20} />}
 81 |           </button>
 82 |           <button
 83 |             className="rounded-lg p-[0.4em]
 84 |               hover:bg-zinc-100 hover:cursor-pointer
 85 |               transition-colors text-zinc-600 hover:text-zinc-800"
 86 |             onClick={startNewConversation}
 87 |           >
 88 |             <SquarePen size={20} />
 89 |           </button>
 90 |         </div>
 91 |         <div
 92 |           className=" h-[calc(100%-3rem)]
 93 |         overflow-y-scroll scrollbar-thin 
 94 |         flex flex-col justify-between border-r border-zinc-200 transition-all duration-300"
 95 |         >
 96 |           <div className="flex flex-col">
 97 |             <ul className="p-2 space-y-1">
 98 |               {conversations.map((conversation) => (
 99 |                 <li
100 |                   key={conversation.id}
101 |                   className={`cursor-pointer p-2 transition-colors rounded-lg ${
102 |                     conversation.id === conversationId || (!conversationId && !conversation.id)
103 |                       ? 'bg-zinc-200 text-zinc-900'
104 |                       : 'hover:bg-zinc-200 text-zinc-600'
105 |                   }`}
106 |                   onClick={() => handleConversationClick(conversation.id)}
107 |                 >
108 |                   <div className="flex items-center justify-between">
109 |                     <span className="truncate flex-grow text-sm">{conversation.title}</span>
110 |                     <div className="flex space-x-2 ml-2">
111 |                       <button
112 |                         className="opacity-60 hover:opacity-100 transition-opacity"
113 |                         onClick={(e) => {
114 |                           e.stopPropagation()
115 |                           const newTitle = prompt('Enter new title:', conversation.title)
116 |                           if (newTitle) editConversationTitle(conversation.id!, newTitle)
117 |                         }}
118 |                       >
119 |                         <Edit size={14} />
120 |                       </button>
121 |                       <button
122 |                         className="opacity-60 hover:opacity-100 transition-opacity"
123 |                         onClick={(e) => {
124 |                           e.stopPropagation()
125 |                           deleteConversation(conversation.id!)
126 |                           setConversationId(undefined)
127 |                         }}
128 |                       >
129 |                         <Trash2 size={14} />
130 |                       </button>
131 |                     </div>
132 |                   </div>
133 |                 </li>
134 |               ))}
135 |             </ul>
136 |           </div>
137 | 
138 |           {/* Model Selector and MCP Servers at bottom */}
139 |           <div className="p-2 border-t border-zinc-200 space-y-3">
140 |             <ModelSelector
141 |               selectedModel={selectedModel}
142 |               onModelChange={onModelChange}
143 |               apiKeyUpdateTrigger={apiKeyUpdateTrigger}
144 |               toolsAvailable={mcpTools.length > 0}
145 |             />
146 |             <McpServers onToolsUpdate={onMcpToolsUpdate} />
147 |           </div>
148 |         </div>
149 |       </div>
150 |     </>
151 |   )
152 | }
153 | 
154 | export default ChatSidebar
155 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/components/OAuthCallback.tsx:
--------------------------------------------------------------------------------
  1 | import React, { useEffect, useState, useRef } from 'react'
  2 | import { useSearchParams } from 'react-router-dom'
  3 | import { completeOAuthFlow } from '../utils/auth'
  4 | import { SupportedProvider } from '../types/models'
  5 | 
  6 | interface OAuthCallbackProps {
  7 |   provider: SupportedProvider
  8 | }
  9 | 
 10 | const OAuthCallback: React.FC<OAuthCallbackProps> = ({ provider }) => {
 11 |   const [searchParams] = useSearchParams()
 12 |   const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading')
 13 |   const [error, setError] = useState<string | null>(null)
 14 |   const executedRef = useRef(false)
 15 | 
 16 |   useEffect(() => {
 17 |     const handleCallback = async () => {
 18 |       if (executedRef.current) {
 19 |         console.log('DEBUG: Skipping duplicate OAuth callback execution')
 20 |         return
 21 |       }
 22 |       executedRef.current = true
 23 | 
 24 |       try {
 25 |         const code = searchParams.get('code')
 26 |         const state = searchParams.get('state')
 27 |         const error = searchParams.get('error')
 28 | 
 29 |         if (error) {
 30 |           throw new Error(`OAuth error: ${error}`)
 31 |         }
 32 | 
 33 |         if (!code) {
 34 |           throw new Error('Missing authorization code')
 35 |         }
 36 | 
 37 |         // OpenRouter doesn't use state parameter, but other providers might
 38 |         const stateToUse = state || 'no-state'
 39 |         await completeOAuthFlow(provider, code, stateToUse)
 40 |         setStatus('success')
 41 | 
 42 |         // Close popup after successful authentication
 43 |         // Give extra time for debugging in development
 44 |         setTimeout(() => {
 45 |           if (window.opener) {
 46 |             window.opener.postMessage({ type: 'oauth_success', provider }, '*')
 47 |             window.close()
 48 |           } else {
 49 |             // Redirect to main page if not in popup
 50 |             window.location.href = '/'
 51 |           }
 52 |         }, 3000)
 53 |       } catch (err) {
 54 |         console.error('OAuth callback error:', err)
 55 |         setError(err instanceof Error ? err.message : 'Unknown error')
 56 |         setStatus('error')
 57 |       }
 58 |     }
 59 | 
 60 |     handleCallback()
 61 |   }, [searchParams, provider])
 62 | 
 63 |   return (
 64 |     <div className="min-h-screen flex items-center justify-center bg-gray-50">
 65 |       <div className="max-w-md w-full bg-white shadow-lg rounded-lg p-8">
 66 |         <div className="text-center">
 67 |           {status === 'loading' && (
 68 |             <>
 69 |               <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
 70 |               <h2 className="text-xl font-semibold text-gray-900 mb-2">Completing Authentication</h2>
 71 |               <p className="text-gray-600">Connecting to {provider}...</p>
 72 |             </>
 73 |           )}
 74 | 
 75 |           {status === 'success' && (
 76 |             <>
 77 |               <div className="text-green-500 mb-4">
 78 |                 <svg className="w-12 h-12 mx-auto" fill="currentColor" viewBox="0 0 20 20">
 79 |                   <path
 80 |                     fillRule="evenodd"
 81 |                     d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
 82 |                     clipRule="evenodd"
 83 |                   />
 84 |                 </svg>
 85 |               </div>
 86 |               <h2 className="text-xl font-semibold text-gray-900 mb-2">Authentication Successful!</h2>
 87 |               <p className="text-gray-600 mb-4">Successfully connected to {provider}. You can now close this window.</p>
 88 |               <button
 89 |                 onClick={() => window.close()}
 90 |                 className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
 91 |               >
 92 |                 Close Window
 93 |               </button>
 94 |             </>
 95 |           )}
 96 | 
 97 |           {status === 'error' && (
 98 |             <>
 99 |               <div className="text-red-500 mb-4">
100 |                 <svg className="w-12 h-12 mx-auto" fill="currentColor" viewBox="0 0 20 20">
101 |                   <path
102 |                     fillRule="evenodd"
103 |                     d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
104 |                     clipRule="evenodd"
105 |                   />
106 |                 </svg>
107 |               </div>
108 |               <h2 className="text-xl font-semibold text-gray-900 mb-2">Authentication Failed</h2>
109 |               <p className="text-gray-600 mb-4">{error || 'An error occurred during authentication'}</p>
110 |               <button
111 |                 onClick={() => window.close()}
112 |                 className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
113 |               >
114 |                 Close Window
115 |               </button>
116 |             </>
117 |           )}
118 |         </div>
119 |       </div>
120 |     </div>
121 |   )
122 | }
123 | 
124 | export default OAuthCallback
125 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/components/ProviderModelsModal.tsx:
--------------------------------------------------------------------------------
  1 | import React, { useState } from 'react'
  2 | import { createPortal } from 'react-dom'
  3 | import { X, Star, Search } from 'lucide-react'
  4 | import { type Model, type Provider } from '../types/models'
  5 | 
  6 | interface ProviderModelsModalProps {
  7 |   isOpen: boolean
  8 |   onClose: () => void
  9 |   provider: Provider | null
 10 |   models: Model[]
 11 |   favorites: string[]
 12 |   onToggleFavorite: (modelId: string) => void
 13 |   isFavorite: (modelId: string) => boolean
 14 |   onModelSelect: (model: Model) => void
 15 |   toolsAvailable?: boolean
 16 | }
 17 | 
 18 | const ProviderModelsModal: React.FC<ProviderModelsModalProps> = ({
 19 |   isOpen,
 20 |   onClose,
 21 |   provider,
 22 |   models,
 23 |   onToggleFavorite,
 24 |   isFavorite,
 25 |   onModelSelect,
 26 |   toolsAvailable = false,
 27 | }) => {
 28 |   const [searchTerm, setSearchTerm] = useState('')
 29 |   const [showToolsOnly, setShowToolsOnly] = useState(false)
 30 | 
 31 |   if (!isOpen || !provider) return null
 32 | 
 33 |   const providerModels = models.filter((model) => model.provider.id === provider.id)
 34 | 
 35 |   const filteredModels = providerModels.filter((model) => {
 36 |     const matchesSearch = model.name.toLowerCase().includes(searchTerm.toLowerCase())
 37 |     const matchesTools = !showToolsOnly || model.supportsTools
 38 |     return matchesSearch && matchesTools
 39 |   })
 40 | 
 41 |   const handleStarClick = (modelId: string, e: React.MouseEvent) => {
 42 |     e.stopPropagation()
 43 |     onToggleFavorite(modelId)
 44 |   }
 45 | 
 46 |   const modalContent = (
 47 |     <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
 48 |       <div className="bg-white rounded-lg shadow-lg max-w-2xl w-full mx-4 max-h-[80vh] overflow-hidden">
 49 |         {/* Header */}
 50 |         <div className="flex items-center justify-between p-4 border-b border-zinc-200">
 51 |           <div className="flex items-center gap-3">
 52 |             <span className="text-2xl">{provider.logo}</span>
 53 |             <div>
 54 |               <h2 className="text-lg font-semibold text-zinc-900">{provider.name} Models</h2>
 55 |               <p className="text-sm text-zinc-500">Select models to add to your favorites</p>
 56 |             </div>
 57 |           </div>
 58 |           <button onClick={onClose} className="p-2 text-zinc-400 hover:text-zinc-600 rounded-md hover:bg-zinc-100">
 59 |             <X size={20} />
 60 |           </button>
 61 |         </div>
 62 | 
 63 |         {/* Search and Filters */}
 64 |         <div className="p-4 border-b border-zinc-200">
 65 |           <div className="relative mb-3">
 66 |             <Search size={16} className="absolute left-3 top-2.5 text-zinc-400" />
 67 |             <input
 68 |               type="text"
 69 |               value={searchTerm}
 70 |               onChange={(e) => setSearchTerm(e.target.value)}
 71 |               placeholder="Search models..."
 72 |               className="w-full pl-10 pr-3 py-2 text-sm border border-zinc-200 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
 73 |             />
 74 |           </div>
 75 | 
 76 |           {toolsAvailable && (
 77 |             <button
 78 |               onClick={() => setShowToolsOnly(!showToolsOnly)}
 79 |               className={`px-3 py-1 text-xs rounded-full border transition-colors ${
 80 |                 showToolsOnly ? 'bg-green-100 text-green-700 border-green-300' : 'bg-white text-zinc-600 border-zinc-200 hover:bg-zinc-50'
 81 |               }`}
 82 |             >
 83 |               🔧 Tools Only
 84 |             </button>
 85 |           )}
 86 |         </div>
 87 | 
 88 |         {/* Models List */}
 89 |         <div className="max-h-96 overflow-y-auto">
 90 |           {filteredModels.length === 0 ? (
 91 |             <div className="p-8 text-center text-zinc-500">No models found matching your criteria</div>
 92 |           ) : (
 93 |             filteredModels.map((model) => (
 94 |               <div
 95 |                 key={model.id}
 96 |                 className="flex items-center justify-between p-4 border-b border-zinc-100 last:border-b-0 hover:bg-zinc-50"
 97 |               >
 98 |                 <div className="flex items-center gap-3 flex-1">
 99 |                   <button
100 |                     onClick={(e) => handleStarClick(model.id, e)}
101 |                     className={`transition-colors ${isFavorite(model.id) ? 'text-yellow-500' : 'text-zinc-300 hover:text-yellow-400'}`}
102 |                   >
103 |                     <Star size={18} fill={isFavorite(model.id) ? 'currentColor' : 'none'} />
104 |                   </button>
105 | 
106 |                   <div className="flex-1">
107 |                     <div className="font-medium text-zinc-900">{model.name}</div>
108 |                     <div className="text-sm text-zinc-500 flex items-center gap-2">
109 |                       {model.supportsTools && <span className="text-green-600">🔧 Tools</span>}
110 |                       {model.reasoning && <span className="text-purple-600">🧠 Reasoning</span>}
111 |                       {model.attachment && <span className="text-blue-600">📎 Attachments</span>}
112 |                       <span className="text-zinc-400">•</span>
113 |                       <span>{(model.contextLimit / 1000).toFixed(0)}K context</span>
114 |                     </div>
115 |                   </div>
116 |                 </div>
117 | 
118 |                 <button
119 |                   onClick={() => onModelSelect(model)}
120 |                   className="px-3 py-1 text-sm bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"
121 |                 >
122 |                   Select
123 |                 </button>
124 |               </div>
125 |             ))
126 |           )}
127 |         </div>
128 | 
129 |         {/* Footer */}
130 |         <div className="p-4 border-t border-zinc-200 bg-zinc-50">
131 |           <div className="text-sm text-zinc-600">
132 |             {filteredModels.length} model{filteredModels.length !== 1 ? 's' : ''} available
133 |             {toolsAvailable && showToolsOnly && ' with tool support'}
134 |           </div>
135 |         </div>
136 |       </div>
137 |     </div>
138 |   )
139 | 
140 |   return createPortal(modalContent, document.body)
141 | }
142 | 
143 | export default ProviderModelsModal
144 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/components/messages/AssistantMessage.tsx:
--------------------------------------------------------------------------------
 1 | import React from 'react'
 2 | import { type AssistantMessage } from '../../types'
 3 | import ReactMarkdown from 'react-markdown'
 4 | import remarkGfm from 'remark-gfm'
 5 | import rehypeHighlight from 'rehype-highlight'
 6 | 
 7 | interface AssistantMessageProps {
 8 |   message: AssistantMessage
 9 | }
10 | 
11 | const AssistantMessage: React.FC<AssistantMessageProps> = ({ message }) => {
12 |   return (
13 |     <div className={`flex justify-start`}>
14 |       <div className={`text-zinc-900 w-full`}>
15 |         <div className="prose prose-zinc prose-tight">
16 |           <ReactMarkdown
17 |             remarkPlugins={[remarkGfm]}
18 |             rehypePlugins={[rehypeHighlight]}
19 |             components={{
20 |               table: ({ children }) => <div className="overflow-x-scroll text-sm">{children}</div>,
21 |             }}
22 |           >
23 |             {message.content}
24 |           </ReactMarkdown>
25 |         </div>
26 |       </div>
27 |     </div>
28 |   )
29 | }
30 | 
31 | export default AssistantMessage
32 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/components/messages/ChatMessage.tsx:
--------------------------------------------------------------------------------
 1 | import React from 'react'
 2 | import ReactMarkdown from 'react-markdown'
 3 | import remarkGfm from 'remark-gfm'
 4 | import rehypeHighlight from 'rehype-highlight'
 5 | import { type Message } from '../../types'
 6 | import ToolCallMessage from './ToolCallMessage.tsx'
 7 | import ToolResultMessage from './ToolResultMessage.tsx'
 8 | import AssistantMessage from './AssistantMessage.tsx'
 9 | import ReasoningMessage from './ReasoningMessage.tsx'
10 | import ErrorMessage from './ErrorMessage.tsx'
11 | 
12 | interface ChatMessageProps {
13 |   message: Message
14 | }
15 | 
16 | const ChatMessage: React.FC<ChatMessageProps> = ({ message }) => {
17 |   // Handle tool calls and results differently
18 |   if (message.role === 'tool-call') {
19 |     return <ToolCallMessage message={message} />
20 |   }
21 | 
22 |   if (message.role === 'tool-result') {
23 |     return <ToolResultMessage message={message} />
24 |   }
25 | 
26 |   if (message.role === 'error') {
27 |     return <ErrorMessage error={message.content} />
28 |   }
29 | 
30 |   if (message.role === 'assistant') {
31 |     if (message.type === 'reasoning') {
32 |       return <ReasoningMessage message={message} />
33 |     } else {
34 |       return <AssistantMessage message={message} />
35 |     }
36 |   }
37 | 
38 |   // Handle regular messages (user, assistant, system)
39 |   return (
40 |     <div className={`flex justify-end`}>
41 |       <div className={`max-w-[80%] rounded-2xl px-5 py-3 shadow bg-white text-black mb-3`}>
42 |         <div className="prose prose-zinc prose-tight">
43 |           <ReactMarkdown
44 |             remarkPlugins={[remarkGfm]}
45 |             rehypePlugins={[rehypeHighlight]}
46 |             components={{
47 |               table: ({ children }) => <div className="overflow-x-scroll text-sm">{children}</div>,
48 |             }}
49 |           >
50 |             {message.content}
51 |           </ReactMarkdown>
52 |         </div>
53 |       </div>
54 |     </div>
55 |   )
56 | }
57 | 
58 | export default ChatMessage
59 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/components/messages/ErrorMessage.tsx:
--------------------------------------------------------------------------------
 1 | import React, { useState } from 'react'
 2 | 
 3 | interface ErrorMessageProps {
 4 |   error: string
 5 | }
 6 | 
 7 | const ErrorMessage: React.FC<ErrorMessageProps> = ({ error }) => {
 8 |   const [isExpanded, setIsExpanded] = useState(false)
 9 | 
10 |   return (
11 |     <div className="bg-red-50 border border-red-200 rounded-lg p-3 my-2">
12 |       <div className="flex items-start gap-2">
13 |         <span className="text-red-500 text-sm font-medium">⚠️ Error:</span>
14 |         <div className="flex-1 min-w-0">
15 |           <div
16 |             className={`text-red-700 text-xs leading-relaxed ${!isExpanded ? 'max-h-[120px] overflow-hidden' : ''}`}
17 |             style={{
18 |               wordBreak: 'break-word',
19 |               whiteSpace: 'pre-wrap',
20 |             }}
21 |           >
22 |             {error}
23 |           </div>
24 |           {error.length > 200 && (
25 |             <button
26 |               onClick={() => setIsExpanded(!isExpanded)}
27 |               className="text-red-600 hover:text-red-800 text-xs mt-1 underline focus:outline-none"
28 |             >
29 |               {isExpanded ? 'Show less' : 'Show more'}
30 |             </button>
31 |           )}
32 |         </div>
33 |       </div>
34 |     </div>
35 |   )
36 | }
37 | 
38 | export default ErrorMessage
39 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/components/messages/ReasoningMessage.tsx:
--------------------------------------------------------------------------------
 1 | import React, { useEffect, useState } from 'react'
 2 | import { type ReasoningMessage } from '../../types'
 3 | 
 4 | interface ReasoningMessageProps {
 5 |   message: ReasoningMessage
 6 | }
 7 | 
 8 | const ReasoningMessage: React.FC<ReasoningMessageProps> = ({ message }) => {
 9 |   const [isExpanded, setIsExpanded] = useState(message.isReasoningStreaming) // Start expanded when streaming
10 | 
11 |   // Auto-collapse instantly when streaming finishes
12 |   useEffect(() => {
13 |     if (!message.isReasoningStreaming && message.reasoningStartTime && message.reasoningEndTime) {
14 |       setIsExpanded(false)
15 |     }
16 |   }, [message.isReasoningStreaming, message.reasoningStartTime, message.reasoningEndTime])
17 | 
18 |   if (!message.content || message.content.trim().length === 0) {
19 |     return null
20 |   }
21 | 
22 |   const toggleExpanded = () => {
23 |     setIsExpanded(!isExpanded)
24 |   }
25 | 
26 |   // Calculate thinking duration
27 |   const duration =
28 |     message.reasoningStartTime && message.reasoningEndTime ? (message.reasoningEndTime - message.reasoningStartTime) / 1000 : null
29 | 
30 |   // Format duration nicely
31 |   const formatDuration = (seconds: number) => {
32 |     if (seconds < 0.001) return `< 1ms`
33 |     if (seconds < 1) return `${Math.round(seconds * 1000)}ms`
34 |     return `${seconds.toFixed(1)}s`
35 |   }
36 | 
37 |   return (
38 |     <div className={`flex justify-start`}>
39 |       <div className={`text-zinc-900 w-full`}>
40 |         <div
41 |           className={`text-xs/5 text-zinc-600 border border-zinc-200 rounded-lg p-3 bg-zinc-50 cursor-pointer hover:bg-zinc-100 ${
42 |             isExpanded ? 'border-zinc-300' : ''
43 |           }`}
44 |           onClick={toggleExpanded}
45 |         >
46 |           <div className="flex items-start gap-2">
47 |             <span className="text-zinc-400 text-xs mt-0.5 flex-shrink-0">💭</span>
48 | 
49 |             {isExpanded ? (
50 |               // Expanded view: show reasoning content
51 |               <div
52 |                 className={`flex-1 ${message.isReasoningStreaming ? 'overflow-hidden whitespace-nowrap flex justify-end' : 'whitespace-pre-wrap'}`}
53 |               >
54 |                 <span className={message.isReasoningStreaming ? 'inline-block' : ''}>
55 |                   {message.content}
56 |                   {message.isReasoningStreaming && <span className="inline-block w-2 h-4 bg-zinc-400 ml-1 animate-pulse"></span>}
57 |                 </span>
58 |               </div>
59 |             ) : (
60 |               // Collapsed view: show timing summary
61 |               <div className="flex-1">
62 |                 <span className="text-zinc-500">
63 |                   {message.isReasoningStreaming
64 |                     ? 'Thinking...'
65 |                     : typeof duration === 'number'
66 |                       ? `Thought for ${formatDuration(duration)}`
67 |                       : 'Thought process'}
68 |                 </span>
69 |               </div>
70 |             )}
71 | 
72 |             <button
73 |               className="text-zinc-400 hover:text-zinc-600 text-xs ml-auto flex-shrink-0"
74 |               onClick={(e) => {
75 |                 e.stopPropagation()
76 |                 toggleExpanded()
77 |               }}
78 |             >
79 |               {/*{isExpanded ? '↑' : '↓'}*/}
80 |             </button>
81 |           </div>
82 |         </div>
83 |       </div>
84 |     </div>
85 |   )
86 | }
87 | 
88 | export default ReasoningMessage
89 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/components/messages/ToolCallMessage.tsx:
--------------------------------------------------------------------------------
 1 | import { useState } from 'react'
 2 | import { ChevronDown, Wrench } from 'lucide-react'
 3 | import { type ToolCallMessage } from '../../types'
 4 | 
 5 | interface ToolCallMessageProps {
 6 |   message: ToolCallMessage
 7 | }
 8 | 
 9 | const ToolCallMessage: React.FC<ToolCallMessageProps> = ({ message }) => {
10 |   const [expanded, setExpanded] = useState(false)
11 | 
12 |   const json = JSON.stringify(message.toolArgs)
13 |   const argsPreview = json.substring(0, 100)
14 |   const shouldTruncate = json.length > 100
15 | 
16 |   return (
17 |     <div className="flex gap-3 py-2 px-4 bg-blue-50 border border-blue-200 rounded-lg">
18 |       <div className={`flex-shrink-0 h-full ${shouldTruncate ? 'mt-1' : 'mt-2.5'}`}>
19 |         <Wrench size={16} className="text-blue-600" />
20 |       </div>
21 |       <div className="flex-grow min-w-0">
22 |         <div className="flex items-center gap-2 mb-1 overflow-hidden">
23 |           <span className="font-medium text-blue-800 text-sm flex-shrink-0">Tool Call</span>
24 |           <span className="text-blue-600 text-sm font-mono bg-blue-100 px-2 py-0.5 rounded">{message.toolName}</span>
25 |           {!shouldTruncate && (
26 |             <div className={`text-sm text-blue-700 font-mono bg-blue-100 p-2 rounded ml-auto`}>
27 |               <span className={'truncate'}>{argsPreview}</span>
28 |             </div>
29 |           )}
30 |         </div>
31 | 
32 |         {shouldTruncate && (
33 |           <div
34 |             className={`text-sm text-blue-700 font-mono bg-blue-100 p-2 rounded cursor-pointer hover:bg-blue-200`}
35 |             onClick={() => setExpanded(!expanded)}
36 |           >
37 |             {expanded ? (
38 |               <pre className="mt-2 whitespace-pre-wrap break-words max-h-64 overflow-y-auto">
39 |                 {JSON.stringify(message.toolArgs, null, 2)}
40 |               </pre>
41 |             ) : (
42 |               <div className="flex items-start justify-between">
43 |                 <span className={'truncate'}>{argsPreview}</span>
44 |                 <button className="ml-2 flex-shrink-0">{<ChevronDown size={14} />}</button>
45 |               </div>
46 |             )}
47 |           </div>
48 |         )}
49 |       </div>
50 |     </div>
51 |   )
52 | }
53 | 
54 | export default ToolCallMessage
55 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/components/messages/ToolResultMessage.tsx:
--------------------------------------------------------------------------------
 1 | import { useState } from 'react'
 2 | import { CheckCircle, ChevronDown } from 'lucide-react'
 3 | import { type ToolResultMessage } from '../../types'
 4 | 
 5 | interface ToolResultMessageProps {
 6 |   message: ToolResultMessage
 7 | }
 8 | 
 9 | const ToolResultMessage: React.FC<ToolResultMessageProps> = ({ message }) => {
10 |   const [expanded, setExpanded] = useState(false)
11 | 
12 |   const json = JSON.stringify(message.toolResult)
13 |   const resultPreview = json.substring(0, 48)
14 |   const shouldTruncate = json.length > 48
15 | 
16 |   return (
17 |     <div className="flex gap-3 py-3 px-4 bg-green-50 border border-green-300 rounded-lg">
18 |       <div className={`flex-shrink-0 h-full ${shouldTruncate ? 'mt-1' : 'mt-2.5'}`}>
19 |         <CheckCircle size={16} className="text-green-600" />
20 |       </div>
21 |       <div className="flex-grow min-w-0">
22 |         <div className="flex items-center gap-2 mb-1 overflow-hidden">
23 |           <span className="font-medium text-green-800 text-sm flex-shrink-0">Tool Result</span>
24 |           <span className="text-green-600 text-sm font-mono bg-green-100 px-2 py-0.5 rounded">{message.toolName}</span>
25 |           {!shouldTruncate && (
26 |             <div className={`text-sm text-green-700 font-mono bg-green-100 p-2 rounded ml-auto`}>
27 |               <span className={'truncate'}>{resultPreview}</span>
28 |             </div>
29 |           )}
30 |         </div>
31 | 
32 |         {shouldTruncate && (
33 |           <div
34 |             className={`text-sm text-green-700 font-mono bg-green-100 p-2 rounded cursor-pointer hover:bg-green-200`}
35 |             onClick={() => setExpanded(!expanded)}
36 |           >
37 |             {expanded ? (
38 |               <pre className="mt-2 whitespace-pre-wrap break-words max-h-64 overflow-y-auto">
39 |                 {JSON.stringify(message.toolResult, null, 2)}
40 |               </pre>
41 |             ) : (
42 |               <div className="flex items-start justify-between">
43 |                 <span className={'truncate'}>{resultPreview}</span>
44 |                 <button className="ml-2 flex-shrink-0">{<ChevronDown size={14} />}</button>
45 |               </div>
46 |             )}
47 |           </div>
48 |         )}
49 |       </div>
50 |     </div>
51 |   )
52 | }
53 | 
54 | export default ToolResultMessage
55 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/consts.ts:
--------------------------------------------------------------------------------
1 | export const storeName = 'conversations'
2 | export const dbName = 'localgpt'
3 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/hooks/useAutoscroll.ts:
--------------------------------------------------------------------------------
 1 | import { useEffect, useRef } from 'react'
 2 | 
 3 | export const useAutoscroll = () => {
 4 |   const messagesEndRef = useRef<HTMLDivElement>(null)
 5 |   const messagesContainerRef = useRef<HTMLDivElement>(null)
 6 |   const shouldAutoScrollRef = useRef(true)
 7 | 
 8 |   useEffect(() => {
 9 |     const container = messagesContainerRef.current
10 |     if (!container) return
11 | 
12 |     const handleScroll = () => {
13 |       const isAtBottom = container.scrollHeight - container.clientHeight - container.scrollTop <= 200
14 |       shouldAutoScrollRef.current = isAtBottom
15 |     }
16 | 
17 |     container.addEventListener('scroll', handleScroll)
18 |     return () => {
19 |       container.removeEventListener('scroll', handleScroll)
20 |     }
21 |   }, [])
22 | 
23 |   const scrollToBottom = (force?: boolean) => {
24 |     if (shouldAutoScrollRef.current || force) {
25 |       messagesEndRef.current?.scrollIntoView({ behavior: force ? 'instant' : 'smooth' })
26 |     }
27 |   }
28 | 
29 |   return {
30 |     messagesEndRef,
31 |     messagesContainerRef,
32 |     scrollToBottom,
33 |   }
34 | }
35 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/hooks/useConversationUpdater.ts:
--------------------------------------------------------------------------------
 1 | import { type Conversation } from '../types'
 2 | 
 3 | interface UseConversationUpdaterProps {
 4 |   conversationId?: number
 5 |   setConversations: React.Dispatch<React.SetStateAction<Conversation[]>>
 6 | }
 7 | 
 8 | export const useConversationUpdater = ({ conversationId, setConversations }: UseConversationUpdaterProps) => {
 9 |   const updateConversation = (updater: (conversation: Conversation) => Conversation) => {
10 |     setConversations((prev) => {
11 |       const conversation = prev.find((c) => c.id === conversationId)
12 |       if (!conversation) {
13 |         console.error(`Missing conversation for ID ${conversationId}`)
14 |         return prev
15 |       }
16 | 
17 |       const updatedConversation = updater(conversation)
18 | 
19 |       return prev.map((c) => (c.id === conversationId ? updatedConversation : c))
20 |     })
21 |   }
22 | 
23 |   return { updateConversation }
24 | }
25 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/hooks/useIndexedDB.ts:
--------------------------------------------------------------------------------
 1 | import { useState, useEffect } from 'react'
 2 | import { openDB, IDBPDatabase } from 'idb'
 3 | import { dbName, storeName } from '../consts'
 4 | 
 5 | export function useIndexedDB() {
 6 |   const [db, setDb] = useState<IDBPDatabase | null>(null)
 7 | 
 8 |   useEffect(() => {
 9 |     const initDb = async () => {
10 |       const db = await openDB(dbName, 1, {
11 |         upgrade(db) {
12 |           db.createObjectStore(storeName, {
13 |             keyPath: 'id',
14 |             autoIncrement: true,
15 |           })
16 |         },
17 |       })
18 |       setDb(db)
19 |     }
20 |     initDb()
21 |   }, [])
22 | 
23 |   return db
24 | }
25 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/hooks/useModels.ts:
--------------------------------------------------------------------------------
  1 | import { useState, useEffect, useCallback } from 'react'
  2 | import { Model, providers, SUPPORTED_PROVIDERS, FAVORITES_KEY } from '../types/models'
  3 | 
  4 | // Load models data from generated JSON
  5 | import modelsData from '../data/models.json'
  6 | 
  7 | export function useModels() {
  8 |   const [models, setModels] = useState<Model[]>([])
  9 |   const [favorites, setFavorites] = useState<string[]>([])
 10 |   const [loading, setLoading] = useState(true)
 11 | 
 12 |   // Load models from data and favorites from localStorage
 13 |   useEffect(() => {
 14 |     const loadModels = () => {
 15 |       const allModels: Model[] = []
 16 | 
 17 |       // Process each provider's models
 18 |       for (const providerId of SUPPORTED_PROVIDERS) {
 19 |         const provider = providers[providerId]
 20 |         const providerModels = modelsData[providerId] || {}
 21 | 
 22 |         for (const [modelId, modelData] of Object.entries(providerModels)) {
 23 |           const model: Model = {
 24 |             id: `${providerId}:${modelId}`,
 25 |             name: modelData.name,
 26 |             provider,
 27 |             modelId,
 28 |             supportsTools: modelData.tool_call,
 29 |             reasoning: modelData.reasoning,
 30 |             attachment: modelData.attachment,
 31 |             contextLimit: modelData.limit.context,
 32 |             outputLimit: modelData.limit.output,
 33 |             cost: modelData.cost,
 34 |           }
 35 |           allModels.push(model)
 36 |         }
 37 |       }
 38 | 
 39 |       setModels(allModels)
 40 |       setLoading(false)
 41 |     }
 42 | 
 43 |     const loadFavorites = () => {
 44 |       try {
 45 |         const saved = localStorage.getItem(FAVORITES_KEY)
 46 |         if (saved) {
 47 |           const parsed = JSON.parse(saved)
 48 |           setFavorites(parsed)
 49 |         }
 50 |       } catch (error) {
 51 |         console.error('Failed to load favorites from localStorage:', error)
 52 |       }
 53 |     }
 54 | 
 55 |     loadModels()
 56 |     loadFavorites()
 57 |   }, [])
 58 | 
 59 |   // Save favorites to localStorage when they change
 60 |   useEffect(() => {
 61 |     // Only save if favorites array has been loaded (not during initial state)
 62 |     if (loading) return
 63 | 
 64 |     try {
 65 |       localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites))
 66 |     } catch (error) {
 67 |       console.error('Failed to save favorites to localStorage:', error)
 68 |     }
 69 |   }, [favorites, loading])
 70 | 
 71 |   const toggleFavorite = useCallback((modelId: string) => {
 72 |     setFavorites((prev) => {
 73 |       if (prev.includes(modelId)) {
 74 |         return prev.filter((id) => id !== modelId)
 75 |       } else {
 76 |         return [...prev, modelId]
 77 |       }
 78 |     })
 79 |   }, [])
 80 | 
 81 |   const addToFavorites = useCallback((modelId: string) => {
 82 |     setFavorites((prev) => {
 83 |       if (!prev.includes(modelId)) {
 84 |         return [...prev, modelId]
 85 |       }
 86 |       return prev
 87 |     })
 88 |   }, [])
 89 | 
 90 |   const isFavorite = useCallback(
 91 |     (modelId: string) => {
 92 |       return favorites.includes(modelId)
 93 |     },
 94 |     [favorites]
 95 |   )
 96 | 
 97 |   const getFavoriteModels = useCallback(() => {
 98 |     return models.filter((model) => favorites.includes(model.id))
 99 |   }, [models, favorites])
100 | 
101 |   const getToolSupportingModels = useCallback(() => {
102 |     return models.filter((model) => model.supportsTools)
103 |   }, [models])
104 | 
105 |   return {
106 |     models,
107 |     favorites,
108 |     loading,
109 |     toggleFavorite,
110 |     addToFavorites,
111 |     isFavorite,
112 |     getFavoriteModels,
113 |     getToolSupportingModels,
114 |   }
115 | }
116 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/hooks/useTheme.ts:
--------------------------------------------------------------------------------
 1 | import { useState, useEffect } from 'react'
 2 | import type { Theme } from '../types'
 3 | 
 4 | export function useTheme() {
 5 |   const [theme, setTheme] = useState<Theme>(() => (localStorage.getItem('theme') as Theme) || 'system')
 6 | 
 7 |   useEffect(() => {
 8 |     const root = window.document.documentElement
 9 |     root.classList.remove('light', 'dark')
10 | 
11 |     const effectiveTheme = theme === 'system' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme
12 | 
13 |     root.classList.add(effectiveTheme)
14 |     if (theme === 'system') {
15 |       localStorage.removeItem('theme')
16 |     } else {
17 |       localStorage.setItem('theme', theme)
18 |     }
19 |   }, [theme])
20 | 
21 |   return [theme, setTheme] as const
22 | }
23 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/index.css:
--------------------------------------------------------------------------------
 1 | @import 'tailwindcss';
 2 | @plugin "@tailwindcss/typography";
 3 | @config "../tailwind.config.js";
 4 | 
 5 | @keyframes hue-rotate {
 6 |   0% {
 7 |     filter: hue-rotate(0deg);
 8 |   }
 9 |   100% {
10 |     filter: hue-rotate(360deg);
11 |   }
12 | }
13 | 
14 | .animated-bg-container {
15 |   position: relative;
16 |   /*overflow: hidden;*/
17 |   /*height: 500vh;*/
18 | }
19 | 
20 | .animated-bg-container::before {
21 |   content: '';
22 |   position: absolute;
23 |   top: 0;
24 |   left: 0;
25 |   right: 0;
26 |   bottom: 0;
27 |   background: repeating-linear-gradient(150deg, #fdf2f8, #faf5ff, #eef2ff, #faf5ff, #fdf2f8 200vmax);
28 |   animation: hue-rotate 60s linear infinite;
29 |   animation-delay: var(--random-delay, 0s);
30 |   z-index: -1;
31 | }
32 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/main.tsx:
--------------------------------------------------------------------------------
 1 | import { StrictMode } from 'react'
 2 | import { createRoot } from 'react-dom/client'
 3 | import './index.css'
 4 | import App from './App.tsx'
 5 | 
 6 | const ENABLE_STRICT_MODE = true
 7 | 
 8 | createRoot(document.getElementById('root')!).render(
 9 |   ENABLE_STRICT_MODE ? (
10 |     <StrictMode>
11 |       <App />
12 |     </StrictMode>
13 |   ) : (
14 |     <App />
15 |   )
16 | )
17 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/styles/github.css:
--------------------------------------------------------------------------------
  1 | .light {
  2 |   pre code.hljs {
  3 |     display: block;
  4 |     overflow-x: auto;
  5 |     padding: 1em;
  6 |   }
  7 |   code.hljs {
  8 |     padding: 3px 5px;
  9 |   }
 10 |   /*!
 11 |     Theme: GitHub
 12 |     Description: Light theme as seen on github.com
 13 |     Author: github.com
 14 |     Maintainer: @Hirse
 15 |     Updated: 2021-05-15
 16 |   
 17 |     Outdated base version: https://github.com/primer/github-syntax-light
 18 |     Current colors taken from GitHub's CSS
 19 |   */
 20 |   .hljs {
 21 |     color: #24292e;
 22 |     background: #ffffff;
 23 |   }
 24 |   .hljs-doctag,
 25 |   .hljs-keyword,
 26 |   .hljs-meta .hljs-keyword,
 27 |   .hljs-template-tag,
 28 |   .hljs-template-variable,
 29 |   .hljs-type,
 30 |   .hljs-variable.language_ {
 31 |     /* prettylights-syntax-keyword */
 32 |     color: #d73a49;
 33 |   }
 34 |   .hljs-title,
 35 |   .hljs-title.class_,
 36 |   .hljs-title.class_.inherited__,
 37 |   .hljs-title.function_ {
 38 |     /* prettylights-syntax-entity */
 39 |     color: #6f42c1;
 40 |   }
 41 |   .hljs-attr,
 42 |   .hljs-attribute,
 43 |   .hljs-literal,
 44 |   .hljs-meta,
 45 |   .hljs-number,
 46 |   .hljs-operator,
 47 |   .hljs-variable,
 48 |   .hljs-selector-attr,
 49 |   .hljs-selector-class,
 50 |   .hljs-selector-id {
 51 |     /* prettylights-syntax-constant */
 52 |     color: #005cc5;
 53 |   }
 54 |   .hljs-regexp,
 55 |   .hljs-string,
 56 |   .hljs-meta .hljs-string {
 57 |     /* prettylights-syntax-string */
 58 |     color: #032f62;
 59 |   }
 60 |   .hljs-built_in,
 61 |   .hljs-symbol {
 62 |     /* prettylights-syntax-variable */
 63 |     color: #e36209;
 64 |   }
 65 |   .hljs-comment,
 66 |   .hljs-code,
 67 |   .hljs-formula {
 68 |     /* prettylights-syntax-comment */
 69 |     color: #6a737d;
 70 |   }
 71 |   .hljs-name,
 72 |   .hljs-quote,
 73 |   .hljs-selector-tag,
 74 |   .hljs-selector-pseudo {
 75 |     /* prettylights-syntax-entity-tag */
 76 |     color: #22863a;
 77 |   }
 78 |   .hljs-subst {
 79 |     /* prettylights-syntax-storage-modifier-import */
 80 |     color: #24292e;
 81 |   }
 82 |   .hljs-section {
 83 |     /* prettylights-syntax-markup-heading */
 84 |     color: #005cc5;
 85 |     font-weight: bold;
 86 |   }
 87 |   .hljs-bullet {
 88 |     /* prettylights-syntax-markup-list */
 89 |     color: #735c0f;
 90 |   }
 91 |   .hljs-emphasis {
 92 |     /* prettylights-syntax-markup-italic */
 93 |     color: #24292e;
 94 |     font-style: italic;
 95 |   }
 96 |   .hljs-strong {
 97 |     /* prettylights-syntax-markup-bold */
 98 |     color: #24292e;
 99 |     font-weight: bold;
100 |   }
101 |   .hljs-addition {
102 |     /* prettylights-syntax-markup-inserted */
103 |     color: #22863a;
104 |     background-color: #f0fff4;
105 |   }
106 |   .hljs-deletion {
107 |     /* prettylights-syntax-markup-deleted */
108 |     color: #b31d28;
109 |     background-color: #ffeef0;
110 |   }
111 |   .hljs-char.escape_,
112 |   .hljs-link,
113 |   .hljs-params,
114 |   .hljs-property,
115 |   .hljs-punctuation,
116 |   .hljs-tag {
117 |     /* purposely ignored */
118 |   }
119 | }
120 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/styles/markdown.css:
--------------------------------------------------------------------------------
 1 | /* Tighter markdown spacing */
 2 | .prose-tight {
 3 |   line-height: 1.5;
 4 | }
 5 | 
 6 | .prose-tight h1,
 7 | .prose-tight h2,
 8 | .prose-tight h3,
 9 | .prose-tight h4,
10 | .prose-tight h5,
11 | .prose-tight h6 {
12 |   margin-top: 1rem;
13 |   margin-bottom: 0.5rem;
14 | }
15 | 
16 | .prose-tight h1:first-child,
17 | .prose-tight h2:first-child,
18 | .prose-tight h3:first-child,
19 | .prose-tight h4:first-child,
20 | .prose-tight h5:first-child,
21 | .prose-tight h6:first-child {
22 |   margin-top: 0;
23 | }
24 | 
25 | .prose-tight p {
26 |   margin-top: 0.75rem;
27 |   margin-bottom: 0.75rem;
28 | }
29 | 
30 | .prose-tight p:first-child {
31 |   margin-top: 0;
32 | }
33 | 
34 | .prose-tight p:last-child {
35 |   margin-bottom: 0;
36 | }
37 | 
38 | .prose-tight ul,
39 | .prose-tight ol {
40 |   margin-top: 0.75rem;
41 |   margin-bottom: 0.75rem;
42 | }
43 | 
44 | .prose-tight li {
45 |   margin-top: 0.25rem;
46 |   margin-bottom: 0.25rem;
47 | }
48 | 
49 | .prose-tight ul > li,
50 | .prose-tight ol > li {
51 |   padding-left: 0.25rem;
52 | }
53 | 
54 | .prose-tight blockquote {
55 |   margin-top: 1rem;
56 |   margin-bottom: 1rem;
57 | }
58 | 
59 | .prose-tight code {
60 |   font-size: 0.875em;
61 | }
62 | 
63 | .prose-tight pre {
64 |   margin-top: 1rem;
65 |   margin-bottom: 1rem;
66 | }
67 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/styles/scrollbar.css:
--------------------------------------------------------------------------------
 1 | /* Light theme scrollbar */
 2 | ::-webkit-scrollbar {
 3 |   width: 8px;
 4 |   height: 8px;
 5 |   /* Scrollbars are fuxing with the layout?? */
 6 |   display: none;
 7 | }
 8 | 
 9 | ::-webkit-scrollbar-track {
10 |   background: #f1f1f1;
11 | }
12 | 
13 | ::-webkit-scrollbar-thumb {
14 |   background: #888;
15 |   border-radius: 4px;
16 | }
17 | 
18 | ::-webkit-scrollbar-thumb:hover {
19 |   background: #555;
20 | }
21 | 
22 | /* Dark theme scrollbar */
23 | .dark ::-webkit-scrollbar {
24 |   width: 8px;
25 |   height: 8px;
26 | }
27 | 
28 | .dark ::-webkit-scrollbar-track {
29 |   background: #27272a; /* zinc-800 */
30 | }
31 | 
32 | .dark ::-webkit-scrollbar-thumb {
33 |   background: #52525b; /* zinc-600 */
34 |   border-radius: 4px;
35 | }
36 | 
37 | .dark ::-webkit-scrollbar-thumb:hover {
38 |   background: #71717a; /* zinc-500 */
39 | }
40 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/types/index.ts:
--------------------------------------------------------------------------------
 1 | export interface BaseMessage {
 2 |   content: string
 3 | }
 4 | 
 5 | export interface UserMessage extends BaseMessage {
 6 |   role: 'user'
 7 | }
 8 | 
 9 | export interface SystemMessage extends BaseMessage {
10 |   role: 'system'
11 | }
12 | 
13 | export interface AssistantMessage extends BaseMessage {
14 |   role: 'assistant'
15 |   type: 'content'
16 | }
17 | 
18 | export interface ReasoningMessage extends BaseMessage {
19 |   role: 'assistant'
20 |   type: 'reasoning'
21 |   reasoningStartTime: number
22 |   reasoningEndTime?: number
23 |   isReasoningStreaming: boolean
24 | }
25 | 
26 | export interface ToolCallMessage {
27 |   role: 'tool-call'
28 |   toolName: string
29 |   toolArgs: Record<string, unknown>
30 |   callId: string
31 | }
32 | 
33 | export interface ToolResultMessage {
34 |   role: 'tool-result'
35 |   toolName: string
36 |   toolArgs: Record<string, unknown>
37 |   toolResult: any
38 |   callId: string
39 | }
40 | 
41 | export interface ErrorMessage {
42 |   role: 'error'
43 |   content: string
44 |   timestamp: number
45 | }
46 | 
47 | export type Message = UserMessage | SystemMessage | AssistantMessage | ReasoningMessage | ToolCallMessage | ToolResultMessage | ErrorMessage
48 | 
49 | export interface Conversation {
50 |   id?: number
51 |   title: string
52 |   messages: Message[]
53 | }
54 | 
55 | export type Theme = 'light' | 'dark' | 'system'
56 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/types/models.ts:
--------------------------------------------------------------------------------
  1 | // Types for models.dev API data
  2 | export interface ModelData {
  3 |   id: string
  4 |   name: string
  5 |   attachment: boolean
  6 |   reasoning: boolean
  7 |   temperature: boolean
  8 |   tool_call: boolean
  9 |   knowledge: string
 10 |   release_date: string
 11 |   last_updated: string
 12 |   modalities: {
 13 |     input: string[]
 14 |     output: string[]
 15 |   }
 16 |   open_weights: boolean
 17 |   limit: {
 18 |     context: number
 19 |     output: number
 20 |   }
 21 |   cost?: {
 22 |     input: number
 23 |     output: number
 24 |     cache_read?: number
 25 |     cache_write?: number
 26 |   }
 27 | }
 28 | 
 29 | export type SupportedProvider = 'anthropic' | 'groq' | 'openrouter'
 30 | 
 31 | export interface Provider {
 32 |   id: SupportedProvider
 33 |   name: string
 34 |   baseUrl: string
 35 |   logo: string
 36 |   documentationUrl: string
 37 |   authType: 'apiKey' | 'oauth'
 38 |   apiKeyHeader?: string
 39 |   oauth?: {
 40 |     authorizeUrl: string
 41 |     tokenUrl: string
 42 |   }
 43 | }
 44 | 
 45 | export interface Model {
 46 |   id: string
 47 |   name: string
 48 |   provider: Provider
 49 |   modelId: string
 50 |   supportsTools: boolean
 51 |   reasoning: boolean
 52 |   attachment: boolean
 53 |   contextLimit: number
 54 |   outputLimit: number
 55 |   cost?: {
 56 |     input: number
 57 |     output: number
 58 |     cache_read?: number
 59 |     cache_write?: number
 60 |   }
 61 | }
 62 | 
 63 | export const providers: Record<SupportedProvider, Provider> = {
 64 |   groq: {
 65 |     id: 'groq',
 66 |     name: 'Groq',
 67 |     baseUrl: 'https://api.groq.com/openai/v1',
 68 |     logo: '🚀',
 69 |     documentationUrl: 'https://console.groq.com/docs',
 70 |     authType: 'apiKey',
 71 |     apiKeyHeader: 'Authorization',
 72 |     // oauth: {
 73 |     //   authorizeUrl: 'http://localhost:3000/keys/request',
 74 |     //   tokenUrl: 'https://openrouter.ai/api/v1/auth/keys'
 75 |     // },
 76 |   },
 77 |   anthropic: {
 78 |     id: 'anthropic',
 79 |     name: 'Anthropic',
 80 |     baseUrl: 'https://api.anthropic.com/v1',
 81 |     logo: '🤖',
 82 |     documentationUrl: 'https://docs.anthropic.com/',
 83 |     authType: 'apiKey',
 84 |     apiKeyHeader: 'x-api-key',
 85 |   },
 86 |   openrouter: {
 87 |     id: 'openrouter',
 88 |     name: 'OpenRouter',
 89 |     baseUrl: 'https://openrouter.ai/api/v1',
 90 |     logo: '🌐',
 91 |     documentationUrl: 'https://openrouter.ai/docs',
 92 |     authType: 'oauth',
 93 |     oauth: {
 94 |       authorizeUrl: 'https://openrouter.ai/auth',
 95 |       tokenUrl: 'https://openrouter.ai/api/v1/auth/keys',
 96 |     },
 97 |   },
 98 | }
 99 | 
100 | export const SUPPORTED_PROVIDERS: readonly SupportedProvider[] = ['anthropic', 'groq', 'openrouter']
101 | 
102 | // Storage keys for user preferences
103 | export const FAVORITES_KEY = 'aiChatTemplate_favorites_v1'
104 | export const PROVIDER_TOKEN_KEY_PREFIX = 'aiChatTemplate_token_'
105 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/utils/apiKeys.ts:
--------------------------------------------------------------------------------
1 | // Re-export functions from new auth system for backward compatibility
2 | export { getApiKey, setApiKey, clearApiKey, hasApiKey } from './auth'
3 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/utils/debugLog.ts:
--------------------------------------------------------------------------------
1 | // Debug logging helper
2 | export const debugLog = (...args: any[]) => {
3 |   if (typeof window !== 'undefined' && localStorage.getItem('USE_MCP_DEBUG') === 'true') {
4 |     console.log(...args)
5 |   }
6 | }
7 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/utils/modelOptions.ts:
--------------------------------------------------------------------------------
 1 | import { type SupportedProvider } from '../types/models'
 2 | import modelsData from '../data/models.json'
 3 | 
 4 | // Helper function to check if a model supports reasoning
 5 | export function supportsReasoning(providerId: SupportedProvider, modelId: string): boolean {
 6 |   const providerModels = modelsData[providerId] as Record<string, any>
 7 |   if (!providerModels) return false
 8 | 
 9 |   const model = providerModels[modelId]
10 |   return model?.reasoning === true
11 | }
12 | 
13 | // Helper function to get provider-specific options
14 | export function getProviderOptions(providerId: SupportedProvider, modelId: string): Record<string, any> | undefined {
15 |   // Add any provider-specific options here
16 |   switch (providerId) {
17 |     case 'groq':
18 |       // Handle specific Groq model options - set reasoningFormat for all reasoning models
19 |       if (supportsReasoning(providerId, modelId)) {
20 |         return {
21 |           groq: {
22 |             reasoningFormat: 'parsed',
23 |           },
24 |         }
25 |       }
26 |       break
27 |     case 'anthropic':
28 |       // Handle specific Anthropic model options
29 |       break
30 |     case 'openrouter':
31 |       // Handle specific OpenRouter model options
32 |       break
33 |   }
34 |   return undefined
35 | }
36 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/utils/modelPreferences.ts:
--------------------------------------------------------------------------------
 1 | import { type Model, providers } from '../types/models'
 2 | 
 3 | // Import models data directly
 4 | import modelsData from '../data/models.json'
 5 | 
 6 | const MODEL_PREFERENCE_KEY = 'aiChatTemplate_selectedModel'
 7 | 
 8 | function getAvailableModels(): Model[] {
 9 |   const models: Model[] = []
10 |   for (const [providerId, providerModels] of Object.entries(modelsData)) {
11 |     const provider = providers[providerId as keyof typeof providers]
12 |     if (!provider) continue
13 | 
14 |     for (const [modelId, modelData] of Object.entries(providerModels)) {
15 |       const model: Model = {
16 |         id: `${providerId}:${modelId}`,
17 |         name: modelData.name,
18 |         provider,
19 |         modelId,
20 |         supportsTools: modelData.tool_call,
21 |         reasoning: modelData.reasoning,
22 |         attachment: modelData.attachment,
23 |         contextLimit: modelData.limit.context,
24 |         outputLimit: modelData.limit.output,
25 |         cost: modelData.cost,
26 |       }
27 |       models.push(model)
28 |     }
29 |   }
30 |   return models
31 | }
32 | 
33 | export const getSelectedModel = (): Model => {
34 |   const saved = localStorage.getItem(MODEL_PREFERENCE_KEY)
35 |   if (saved) {
36 |     try {
37 |       const parsed = JSON.parse(saved)
38 |       // Find the model by ID to ensure it still exists
39 |       const availableModels = getAvailableModels()
40 |       const model = availableModels.find((m) => m.id === parsed.id)
41 |       if (model) {
42 |         return model
43 |       }
44 |     } catch (e) {
45 |       console.warn('Failed to parse saved model preference:', e)
46 |     }
47 |   }
48 |   // Default to first available model
49 |   const availableModels = getAvailableModels()
50 |   return availableModels[0]
51 | }
52 | 
53 | export const setSelectedModel = (model: Model): void => {
54 |   localStorage.setItem(MODEL_PREFERENCE_KEY, JSON.stringify({ id: model.id }))
55 | }
56 | 


--------------------------------------------------------------------------------
/examples/chat-ui/src/vite-env.d.ts:
--------------------------------------------------------------------------------
 1 | /// <reference types="vite/client" />
 2 | 
 3 | declare global {
 4 |   interface Window {
 5 |     apiKeyModalResolve?: (value: boolean) => void
 6 |   }
 7 | }
 8 | 
 9 | export {}
10 | 


--------------------------------------------------------------------------------
/examples/chat-ui/tailwind.config.js:
--------------------------------------------------------------------------------
 1 | /** @type {import('tailwindcss').Config} */
 2 | module.exports = {
 3 |   theme: {
 4 |     extend: {
 5 |       typography: {
 6 |         DEFAULT: {
 7 |           css: {
 8 |             pre: {
 9 |               padding: '0',
10 |               filter: 'brightness(96%)',
11 |               border: '0',
12 |               backgroundColor: 'transparent',
13 |             },
14 |           },
15 |         },
16 |       },
17 |     },
18 |   },
19 | }
20 | 


--------------------------------------------------------------------------------
/examples/chat-ui/tsconfig.app.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
 4 |     "target": "ES2020",
 5 |     "useDefineForClassFields": true,
 6 |     "lib": ["ES2022", "DOM", "DOM.Iterable"],
 7 |     "module": "ESNext",
 8 |     "skipLibCheck": true,
 9 | 
10 |     /* Bundler mode */
11 |     "moduleResolution": "bundler",
12 |     "allowImportingTsExtensions": true,
13 |     "isolatedModules": true,
14 |     "moduleDetection": "force",
15 |     "noEmit": true,
16 |     "jsx": "react-jsx",
17 | 
18 |     /* Linting */
19 |     "strict": true,
20 |     "noUnusedLocals": true,
21 |     "noUnusedParameters": true,
22 |     "noFallthroughCasesInSwitch": true,
23 |     "noUncheckedSideEffectImports": true
24 |   },
25 |   "include": ["src"]
26 | }
27 | 


--------------------------------------------------------------------------------
/examples/chat-ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 |   "compilerOptions": {
3 |     "noUnusedLocals": false,
4 |     "noUnusedParameters": false
5 |   },
6 |   "files": [],
7 |   "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }, { "path": "./tsconfig.worker.json" }]
8 | }
9 | 


--------------------------------------------------------------------------------
/examples/chat-ui/tsconfig.node.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
 4 |     "target": "ES2022",
 5 |     "lib": ["ES2023"],
 6 |     "module": "ESNext",
 7 |     "skipLibCheck": true,
 8 | 
 9 |     /* Bundler mode */
10 |     "moduleResolution": "bundler",
11 |     "allowImportingTsExtensions": true,
12 |     "isolatedModules": true,
13 |     "moduleDetection": "force",
14 |     "noEmit": true,
15 | 
16 |     /* Linting */
17 |     "strict": true,
18 |     "noUnusedLocals": true,
19 |     "noUnusedParameters": true,
20 |     "noFallthroughCasesInSwitch": true,
21 |     "noUncheckedSideEffectImports": true
22 |   },
23 |   "include": ["vite.config.ts"]
24 | }
25 | 


--------------------------------------------------------------------------------
/examples/chat-ui/tsconfig.worker.json:
--------------------------------------------------------------------------------
1 | {
2 |   "extends": "./tsconfig.node.json",
3 |   "compilerOptions": {
4 |     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.worker.tsbuildinfo",
5 |     "types": ["@cloudflare/workers-types/2023-07-01", "vite/client"]
6 |   },
7 |   "include": ["api"]
8 | }
9 | 


--------------------------------------------------------------------------------
/examples/chat-ui/vite.config.ts:
--------------------------------------------------------------------------------
 1 | import { defineConfig } from 'vite'
 2 | import react from '@vitejs/plugin-react'
 3 | import { cloudflare } from '@cloudflare/vite-plugin'
 4 | import tailwindcss from '@tailwindcss/vite'
 5 | 
 6 | // https://vite.dev/config/
 7 | export default defineConfig({
 8 |   plugins: [react(), tailwindcss(), cloudflare()],
 9 | })
10 | 


--------------------------------------------------------------------------------
/examples/chat-ui/wrangler.jsonc:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * For more details on how to configure Wrangler, refer to:
 3 |  * https://developers.cloudflare.com/workers/wrangler/configuration/
 4 |  */
 5 | {
 6 |   "$schema": "node_modules/wrangler/config-schema.json",
 7 |   "name": "chat-example-use-mcp",
 8 |   "workers_dev": true,
 9 |   "routes": [
10 |     {
11 |       "pattern": "chat.use-mcp.dev",
12 |       "custom_domain": true,
13 |     },
14 |   ],
15 |   //  "main": "api/index.ts",
16 |   "compatibility_date": "2025-02-07",
17 |   //  "ai": {
18 |   //    "binding": "AI"
19 |   //  },
20 |   "assets": {
21 |     "not_found_handling": "single-page-application",
22 |     //    "binding": "ASSETS",
23 |     "directory": "dist/client",
24 |   },
25 |   "observability": {
26 |     "enabled": true,
27 |   },
28 |   /**
29 |    * Smart Placement
30 |    * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
31 |    */
32 |   // "placement": { "mode": "smart" },
33 | 
34 |   /**
35 |    * Bindings
36 |    * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
37 |    * databases, object storage, AI inference, real-time communication and more.
38 |    * https://developers.cloudflare.com/workers/runtime-apis/bindings/
39 |    */
40 | 
41 |   /**
42 |    * Environment Variables
43 |    * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
44 |    */
45 |   // "vars": { "MY_VARIABLE": "production_value" },
46 |   /**
47 |    * Note: Use secrets to store sensitive data.
48 |    * https://developers.cloudflare.com/workers/configuration/secrets/
49 |    */
50 | 
51 |   /**
52 |    * Static Assets
53 |    * https://developers.cloudflare.com/workers/static-assets/binding/
54 |    */
55 |   // "assets": { "directory": "./public/", "binding": "ASSETS" },
56 | 
57 |   /**
58 |    * Service Bindings (communicate between multiple Workers)
59 |    * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
60 |    */
61 |   // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
62 | }
63 | 


--------------------------------------------------------------------------------
/examples/inspector/.gitignore:
--------------------------------------------------------------------------------
 1 | # Logs
 2 | logs
 3 | *.log
 4 | npm-debug.log*
 5 | yarn-debug.log*
 6 | yarn-error.log*
 7 | pnpm-debug.log*
 8 | lerna-debug.log*
 9 | 
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | 
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | 
26 | .wrangler
27 | 
28 | # Playwright test results
29 | test-results/
30 | playwright-report/


--------------------------------------------------------------------------------
/examples/inspector/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 |   "trailingComma": "es5",
3 |   "tabWidth": 2,
4 |   "semi": false,
5 |   "singleQuote": true,
6 |   "printWidth": 140
7 | }
8 | 


--------------------------------------------------------------------------------
/examples/inspector/AGENT.md:
--------------------------------------------------------------------------------
 1 | # AI Chat Template - Development Guide
 2 | 
 3 | ## Commands
 4 | - **Dev server**: `pnpm dev`
 5 | - **Build**: `pnpm build` (runs TypeScript compilation then Vite build)
 6 | - **Lint**: `pnpm lint` (ESLint)
 7 | - **Deploy**: `pnpm deploy` (builds and deploys with Wrangler)
 8 | - **Test**: `pnpm test` (Playwright E2E tests)
 9 | - **Test UI**: `pnpm test:ui` (Playwright test runner with UI)
10 | - **Test headed**: `pnpm test:headed` (Run tests in visible browser)
11 | 
12 | ## Code Style
13 | - **Formatting**: Prettier with 2-space tabs, single quotes, no semicolons, 140 char line width
14 | - **Imports**: Use `.tsx`/`.ts` extensions in imports, group by external/internal
15 | - **Components**: React FC with explicit typing, PascalCase names
16 | - **Hooks**: Custom hooks start with `use`, camelCase
17 | - **Types**: Define interfaces in `src/types/index.ts`, use `type` for unions
18 | - **Files**: Use PascalCase for components, camelCase for hooks/utilities
19 | - **State**: Use proper TypeScript typing for all state variables
20 | - **Error handling**: Use try/catch blocks with proper error propagation
21 | - **Database**: IndexedDB with typed interfaces, async/await pattern
22 | - **Styling**: Tailwind CSS classes, responsive design patterns
23 | 
24 | ## Tech Stack
25 | React 19, TypeScript, Vite, Tailwind CSS, Hono API, Cloudflare Workers, IndexedDB
26 | 


--------------------------------------------------------------------------------
/examples/inspector/README.md:
--------------------------------------------------------------------------------
 1 | # MCP Inspector
 2 | 
 3 | A minimal demo showcasing the `use-mcp` React hook for connecting to Model Context Protocol (MCP) servers.
 4 | 
 5 | ## Features
 6 | 
 7 | - Connect to any MCP server via URL
 8 | - View available tools and their schemas
 9 | - Browse and read server resources
10 | - Interact with server-provided prompts
11 | - Real-time connection status monitoring
12 | - Debug logging for troubleshooting
13 | - Clean, minimal UI focused on MCP functionality
14 | 
15 | ## Getting Started
16 | 
17 | 0. Make sure you've built the parent `use-mcp` directory at least once!
18 | ```bash
19 | cd ../.. && pnpm build && cd -
20 | ```
21 | 
22 | 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.
23 | 
24 | 1. Install dependencies:
25 | ```bash
26 | pnpm install
27 | ```
28 | 
29 | 2. Start the development server:
30 | ```bash
31 | pnpm dev
32 | ```
33 | 
34 | 3. Open your browser and navigate to the displayed local URL
35 | 
36 | 4. Enter an MCP server URL to test the connection and explore available tools, resources, and prompts
37 | 
38 | ## What This Demonstrates
39 | 
40 | 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:
41 | 
42 | ```tsx
43 | import { useMcp } from 'use-mcp/react'
44 | 
45 | const connection = useMcp({
46 |   url: 'your-mcp-server-url',
47 |   debug: true,
48 |   autoRetry: false
49 | })
50 | 
51 | // Access connection.state, connection.tools, connection.resources, 
52 | // connection.prompts, connection.error, etc.
53 | ```
54 | 
55 | The `McpServers` component wraps this hook to provide a complete UI for server management, tool inspection, resource browsing, and prompt interaction.
56 | 
57 | ## Supported MCP Features
58 | 
59 | - **Tools**: Execute server-provided tools with custom arguments and view results
60 | - **Resources**: Browse available resources and read their contents (text or binary)
61 | - **Resource Templates**: View dynamic resource templates with URI patterns
62 | - **Prompts**: Interact with server prompts, provide arguments, and view generated messages
63 | 
64 | Note: Not all MCP servers implement all features. The inspector will gracefully handle servers that only support a subset of the MCP specification.
65 | 


--------------------------------------------------------------------------------
/examples/inspector/eslint.config.js:
--------------------------------------------------------------------------------
 1 | import js from '@eslint/js'
 2 | import globals from 'globals'
 3 | import reactHooks from 'eslint-plugin-react-hooks'
 4 | import reactRefresh from 'eslint-plugin-react-refresh'
 5 | import tseslint from 'typescript-eslint'
 6 | 
 7 | export default tseslint.config(
 8 |   { ignores: ['dist'] },
 9 |   {
10 |     extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 |     files: ['**/*.{ts,tsx}'],
12 |     languageOptions: {
13 |       ecmaVersion: 2020,
14 |       globals: globals.browser,
15 |     },
16 |     plugins: {
17 |       'react-hooks': reactHooks,
18 |       'react-refresh': reactRefresh,
19 |     },
20 |     rules: {
21 |       ...reactHooks.configs.recommended.rules,
22 |       'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
23 |     },
24 |   }
25 | )
26 | 


--------------------------------------------------------------------------------
/examples/inspector/index.html:
--------------------------------------------------------------------------------
 1 | <!doctype html>
 2 | <html lang="en">
 3 |   <head>
 4 |     <meta charset="UTF-8" />
 5 |     <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
 6 |     <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
 7 |     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 8 | 
 9 |     <!-- Primary Meta Tags -->
10 |     <title>MCP Inspector - use-mcp React Hook Demo</title>
11 |     <meta
12 |       name="description"
13 |       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."
14 |     />
15 | 
16 |     <!-- Open Graph / Facebook -->
17 |     <meta property="og:type" content="website" />
18 |     <meta property="og:site_name" content="MCP Inspector" />
19 |     <meta property="og:title" content="MCP Inspector - use-mcp React Hook Demo" />
20 |     <meta
21 |       property="og:description"
22 |       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."
23 |     />
24 |     <meta property="og:image" content="https://inspector.use-mcp.dev/use-mcp-card.png" />
25 |     <meta property="og:image:width" content="1200" />
26 |     <meta property="og:image:height" content="630" />
27 |     <meta property="og:url" content="https://inspector.use-mcp.dev" />
28 | 
29 |     <!-- Twitter -->
30 |     <meta name="twitter:card" content="summary_large_image" />
31 |     <meta name="twitter:title" content="MCP Inspector - use-mcp React Hook Demo" />
32 |     <meta
33 |       name="twitter:description"
34 |       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."
35 |     />
36 |     <meta name="twitter:image" content="https://inspector.use-mcp.dev/use-mcp-card.png" />
37 | 
38 |     <!-- Additional Meta Tags -->
39 |     <meta name="author" content="Glen Maddern" />
40 |     <meta name="keywords" content="use-mcp, Model Context Protocol, MCP, AI, LLM, tools, inspector, debugging, development, React hook" />
41 |     <meta name="application-name" content="MCP Inspector" />
42 |     <meta name="apple-mobile-web-app-title" content="MCP Inspector" />
43 |     <meta name="theme-color" content="#ffffff" />
44 | 
45 |     <!-- Canonical URL -->
46 |     <link rel="canonical" href="https://inspector.use-mcp.dev" />
47 |   </head>
48 |   <body>
49 |     <div id="root"></div>
50 |     <script type="module" src="/src/main.tsx"></script>
51 |   </body>
52 | </html>
53 | 


--------------------------------------------------------------------------------
/examples/inspector/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "mcp-inspector",
 3 |   "private": true,
 4 |   "version": "0.0.0",
 5 |   "type": "module",
 6 |   "scripts": {
 7 |     "dev": "vite --port=5001",
 8 |     "build": "tsc -b && pnpm run build:only",
 9 |     "build:only": "vite build",
10 |     "lint": "eslint .",
11 |     "preview": "vite preview",
12 |     "deploy": "pnpm run build:only && wrangler deploy"
13 |   },
14 |   "dependencies": {
15 |     "@tailwindcss/vite": "^4.0.14",
16 |     "lucide-react": "^0.482.0",
17 |     "react": "^19.0.0",
18 |     "react-dom": "^19.0.0",
19 |     "react-router-dom": "^7.6.2",
20 |     "tailwindcss": "^4.0.14"
21 |   },
22 |   "devDependencies": {
23 |     "@cloudflare/vite-plugin": "^1.5.0",
24 |     "@eslint/js": "^9.21.0",
25 |     "@tailwindcss/typography": "^0.5.16",
26 |     "@types/react": "^19.0.10",
27 |     "@types/react-dom": "^19.0.4",
28 |     "@vitejs/plugin-react": "^4.3.4",
29 |     "eslint": "^9.21.0",
30 |     "eslint-plugin-react-hooks": "^5.1.0",
31 |     "eslint-plugin-react-refresh": "^0.4.19",
32 |     "globals": "^15.15.0",
33 |     "prettier": "^3.5.3",
34 |     "typescript": "~5.7.2",
35 |     "typescript-eslint": "^8.24.1",
36 |     "use-mcp": "link:../..",
37 |     "vite": "^6.2.0",
38 |     "wrangler": "^4.19.1"
39 |   },
40 |   "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
41 | }
42 | 


--------------------------------------------------------------------------------
/examples/inspector/public/vite.svg:
--------------------------------------------------------------------------------
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>


--------------------------------------------------------------------------------
/examples/inspector/src/App.tsx:
--------------------------------------------------------------------------------
 1 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
 2 | import { McpServers } from './components/McpServers.js'
 3 | import { OAuthCallback } from './components/OAuthCallback.js'
 4 | 
 5 | function App() {
 6 |   return (
 7 |     <Router>
 8 |       <Routes>
 9 |         <Route path="/oauth/callback" element={<OAuthCallback />} />
10 |         <Route
11 |           path="/"
12 |           element={
13 |             <div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
14 |               <div className="w-full max-w-2xl">
15 |                 <div className="text-center mb-8">
16 |                   <h1 className="text-3xl font-bold text-gray-900 mb-2">MCP Inspector</h1>
17 |                   <p className="text-gray-600">
18 |                     Minimal demo showcasing the{' '}
19 |                     <a
20 |                       href="https://github.com/modelcontextprotocol/use-mcp"
21 |                       target="_blank"
22 |                       rel="noopener noreferrer"
23 |                       className="text-blue-600 hover:text-blue-800 underline font-medium transition-colors"
24 |                     >
25 |                       use-mcp
26 |                     </a>{' '}
27 |                     React hook
28 |                   </p>
29 |                 </div>
30 |                 <McpServers />
31 |               </div>
32 |             </div>
33 |           }
34 |         />
35 |       </Routes>
36 |     </Router>
37 |   )
38 | }
39 | 
40 | export default App
41 | 


--------------------------------------------------------------------------------
/examples/inspector/src/components/OAuthCallback.tsx:
--------------------------------------------------------------------------------
 1 | import { useEffect } from 'react'
 2 | import { onMcpAuthorization } from 'use-mcp'
 3 | 
 4 | export function OAuthCallback() {
 5 |   useEffect(() => {
 6 |     onMcpAuthorization()
 7 |   }, [])
 8 | 
 9 |   return (
10 |     <div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
11 |       <div className="text-center">
12 |         <h1 className="text-2xl font-bold text-gray-900 mb-4">Authenticating...</h1>
13 |         <p className="text-gray-600 mb-2">Please wait while we complete your authentication.</p>
14 |         <p className="text-sm text-gray-500">This window should close automatically.</p>
15 |       </div>
16 |     </div>
17 |   )
18 | }
19 | 


--------------------------------------------------------------------------------
/examples/inspector/src/index.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss';
2 | @plugin "@tailwindcss/typography";
3 | @config "../tailwind.config.js";
4 | 


--------------------------------------------------------------------------------
/examples/inspector/src/main.tsx:
--------------------------------------------------------------------------------
 1 | import { StrictMode } from 'react'
 2 | import { createRoot } from 'react-dom/client'
 3 | import './index.css'
 4 | import App from './App.tsx'
 5 | 
 6 | createRoot(document.getElementById('root')!).render(
 7 |   <StrictMode>
 8 |     <App />
 9 |   </StrictMode>
10 | )
11 | 


--------------------------------------------------------------------------------
/examples/inspector/src/styles/github.css:
--------------------------------------------------------------------------------
  1 | .light {
  2 |   pre code.hljs {
  3 |     display: block;
  4 |     overflow-x: auto;
  5 |     padding: 1em;
  6 |   }
  7 |   code.hljs {
  8 |     padding: 3px 5px;
  9 |   }
 10 |   /*!
 11 |     Theme: GitHub
 12 |     Description: Light theme as seen on github.com
 13 |     Author: github.com
 14 |     Maintainer: @Hirse
 15 |     Updated: 2021-05-15
 16 |   
 17 |     Outdated base version: https://github.com/primer/github-syntax-light
 18 |     Current colors taken from GitHub's CSS
 19 |   */
 20 |   .hljs {
 21 |     color: #24292e;
 22 |     background: #ffffff;
 23 |   }
 24 |   .hljs-doctag,
 25 |   .hljs-keyword,
 26 |   .hljs-meta .hljs-keyword,
 27 |   .hljs-template-tag,
 28 |   .hljs-template-variable,
 29 |   .hljs-type,
 30 |   .hljs-variable.language_ {
 31 |     /* prettylights-syntax-keyword */
 32 |     color: #d73a49;
 33 |   }
 34 |   .hljs-title,
 35 |   .hljs-title.class_,
 36 |   .hljs-title.class_.inherited__,
 37 |   .hljs-title.function_ {
 38 |     /* prettylights-syntax-entity */
 39 |     color: #6f42c1;
 40 |   }
 41 |   .hljs-attr,
 42 |   .hljs-attribute,
 43 |   .hljs-literal,
 44 |   .hljs-meta,
 45 |   .hljs-number,
 46 |   .hljs-operator,
 47 |   .hljs-variable,
 48 |   .hljs-selector-attr,
 49 |   .hljs-selector-class,
 50 |   .hljs-selector-id {
 51 |     /* prettylights-syntax-constant */
 52 |     color: #005cc5;
 53 |   }
 54 |   .hljs-regexp,
 55 |   .hljs-string,
 56 |   .hljs-meta .hljs-string {
 57 |     /* prettylights-syntax-string */
 58 |     color: #032f62;
 59 |   }
 60 |   .hljs-built_in,
 61 |   .hljs-symbol {
 62 |     /* prettylights-syntax-variable */
 63 |     color: #e36209;
 64 |   }
 65 |   .hljs-comment,
 66 |   .hljs-code,
 67 |   .hljs-formula {
 68 |     /* prettylights-syntax-comment */
 69 |     color: #6a737d;
 70 |   }
 71 |   .hljs-name,
 72 |   .hljs-quote,
 73 |   .hljs-selector-tag,
 74 |   .hljs-selector-pseudo {
 75 |     /* prettylights-syntax-entity-tag */
 76 |     color: #22863a;
 77 |   }
 78 |   .hljs-subst {
 79 |     /* prettylights-syntax-storage-modifier-import */
 80 |     color: #24292e;
 81 |   }
 82 |   .hljs-section {
 83 |     /* prettylights-syntax-markup-heading */
 84 |     color: #005cc5;
 85 |     font-weight: bold;
 86 |   }
 87 |   .hljs-bullet {
 88 |     /* prettylights-syntax-markup-list */
 89 |     color: #735c0f;
 90 |   }
 91 |   .hljs-emphasis {
 92 |     /* prettylights-syntax-markup-italic */
 93 |     color: #24292e;
 94 |     font-style: italic;
 95 |   }
 96 |   .hljs-strong {
 97 |     /* prettylights-syntax-markup-bold */
 98 |     color: #24292e;
 99 |     font-weight: bold;
100 |   }
101 |   .hljs-addition {
102 |     /* prettylights-syntax-markup-inserted */
103 |     color: #22863a;
104 |     background-color: #f0fff4;
105 |   }
106 |   .hljs-deletion {
107 |     /* prettylights-syntax-markup-deleted */
108 |     color: #b31d28;
109 |     background-color: #ffeef0;
110 |   }
111 |   .hljs-char.escape_,
112 |   .hljs-link,
113 |   .hljs-params,
114 |   .hljs-property,
115 |   .hljs-punctuation,
116 |   .hljs-tag {
117 |     /* purposely ignored */
118 |   }
119 | }
120 | 


--------------------------------------------------------------------------------
/examples/inspector/src/styles/scrollbar.css:
--------------------------------------------------------------------------------
 1 | /* Light theme scrollbar */
 2 | ::-webkit-scrollbar {
 3 |   width: 8px;
 4 |   height: 8px;
 5 | }
 6 | 
 7 | ::-webkit-scrollbar-track {
 8 |   background: #f1f1f1;
 9 | }
10 | 
11 | ::-webkit-scrollbar-thumb {
12 |   background: #888;
13 |   border-radius: 4px;
14 | }
15 | 
16 | ::-webkit-scrollbar-thumb:hover {
17 |   background: #555;
18 | }
19 | 
20 | /* Dark theme scrollbar */
21 | .dark ::-webkit-scrollbar {
22 |   width: 8px;
23 |   height: 8px;
24 | }
25 | 
26 | .dark ::-webkit-scrollbar-track {
27 |   background: #27272a; /* zinc-800 */
28 | }
29 | 
30 | .dark ::-webkit-scrollbar-thumb {
31 |   background: #52525b; /* zinc-600 */
32 |   border-radius: 4px;
33 | }
34 | 
35 | .dark ::-webkit-scrollbar-thumb:hover {
36 |   background: #71717a; /* zinc-500 */
37 | }
38 | 


--------------------------------------------------------------------------------
/examples/inspector/src/vite-env.d.ts:
--------------------------------------------------------------------------------
 1 | /// <reference types="vite/client" />
 2 | 
 3 | declare global {
 4 |   interface Window {
 5 |     apiKeyModalResolve?: (value: boolean) => void
 6 |   }
 7 | }
 8 | 
 9 | export {}
10 | 


--------------------------------------------------------------------------------
/examples/inspector/tailwind.config.js:
--------------------------------------------------------------------------------
 1 | /** @type {import('tailwindcss').Config} */
 2 | module.exports = {
 3 |   theme: {
 4 |     extend: {
 5 |       typography: {
 6 |         DEFAULT: {
 7 |           css: {
 8 |             pre: {
 9 |               padding: '0',
10 |               filter: 'brightness(96%)',
11 |               border: '0',
12 |               backgroundColor: 'transparent',
13 |             },
14 |           },
15 |         },
16 |       },
17 |     },
18 |   },
19 | }
20 | 


--------------------------------------------------------------------------------
/examples/inspector/tsconfig.app.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
 4 |     "target": "ES2020",
 5 |     "useDefineForClassFields": true,
 6 |     "lib": ["ES2020", "DOM", "DOM.Iterable"],
 7 |     "module": "ESNext",
 8 |     "skipLibCheck": true,
 9 | 
10 |     /* Bundler mode */
11 |     "moduleResolution": "bundler",
12 |     "allowImportingTsExtensions": true,
13 |     "isolatedModules": true,
14 |     "moduleDetection": "force",
15 |     "noEmit": true,
16 |     "jsx": "react-jsx",
17 | 
18 |     /* Linting */
19 |     "strict": true,
20 |     "noUnusedLocals": true,
21 |     "noUnusedParameters": true,
22 |     "noFallthroughCasesInSwitch": true,
23 |     "noUncheckedSideEffectImports": true
24 |   },
25 |   "include": ["src"]
26 | }
27 | 


--------------------------------------------------------------------------------
/examples/inspector/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 |   "compilerOptions": {
3 |     "noUnusedLocals": false,
4 |     "noUnusedParameters": false
5 |   },
6 |   "files": [],
7 |   "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
8 | }
9 | 


--------------------------------------------------------------------------------
/examples/inspector/tsconfig.node.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
 4 |     "target": "ES2022",
 5 |     "lib": ["ES2023"],
 6 |     "module": "ESNext",
 7 |     "skipLibCheck": true,
 8 | 
 9 |     /* Bundler mode */
10 |     "moduleResolution": "bundler",
11 |     "allowImportingTsExtensions": true,
12 |     "isolatedModules": true,
13 |     "moduleDetection": "force",
14 |     "noEmit": true,
15 | 
16 |     /* Linting */
17 |     "strict": true,
18 |     "noUnusedLocals": true,
19 |     "noUnusedParameters": true,
20 |     "noFallthroughCasesInSwitch": true,
21 |     "noUncheckedSideEffectImports": true
22 |   },
23 |   "include": ["vite.config.ts"]
24 | }
25 | 


--------------------------------------------------------------------------------
/examples/inspector/vite.config.ts:
--------------------------------------------------------------------------------
 1 | import { defineConfig } from 'vite'
 2 | import react from '@vitejs/plugin-react'
 3 | import tailwindcss from '@tailwindcss/vite'
 4 | 
 5 | // https://vite.dev/config/
 6 | export default defineConfig({
 7 |   plugins: [react(), tailwindcss()],
 8 |   build: {
 9 |     // @ts-expect-error I don't want to install @types/node for this one line
10 |     minify: process.env.NO_MINIFY !== 'true',
11 |   },
12 | })
13 | 


--------------------------------------------------------------------------------
/examples/inspector/wrangler.jsonc:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * For more details on how to configure Wrangler, refer to:
 3 |  * https://developers.cloudflare.com/workers/wrangler/configuration/
 4 |  */
 5 | {
 6 |   "$schema": "node_modules/wrangler/config-schema.json",
 7 |   "name": "inspector-use-mcp",
 8 |   "routes": [
 9 |     {
10 |       "pattern": "inspector.use-mcp.dev",
11 |       "custom_domain": true,
12 |     },
13 |   ],
14 |   "workers_dev": true,
15 |   //  "main": "api/index.ts",
16 |   "compatibility_date": "2025-02-07",
17 |   //  "ai": {
18 |   //    "binding": "AI"
19 |   //  },
20 |   "assets": {
21 |     "not_found_handling": "single-page-application",
22 |     "directory": "dist",
23 |   },
24 |   "observability": {
25 |     "enabled": true,
26 |   },
27 |   /**
28 |    * Smart Placement
29 |    * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
30 |    */
31 |   // "placement": { "mode": "smart" },
32 |   /**
33 |    * Bindings
34 |    * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
35 |    * databases, object storage, AI inference, real-time communication and more.
36 |    * https://developers.cloudflare.com/workers/runtime-apis/bindings/
37 |    */
38 |   /**
39 |    * Environment Variables
40 |    * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
41 |    */
42 |   // "vars": { "MY_VARIABLE": "production_value" },
43 |   /**
44 |    * Note: Use secrets to store sensitive data.
45 |    * https://developers.cloudflare.com/workers/configuration/secrets/
46 |    */
47 |   /**
48 |    * Static Assets
49 |    * https://developers.cloudflare.com/workers/static-assets/binding/
50 |    */
51 |   // "assets": { "directory": "./public/", "binding": "ASSETS" },
52 |   /**
53 |    * Service Bindings (communicate between multiple Workers)
54 |    * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
55 |    */
56 |   // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
57 | }
58 | 


--------------------------------------------------------------------------------
/examples/servers/cf-agents/.gitignore:
--------------------------------------------------------------------------------
  1 | # Logs
  2 | 
  3 | logs
  4 | _.log
  5 | npm-debug.log_
  6 | yarn-debug.log*
  7 | yarn-error.log*
  8 | lerna-debug.log*
  9 | .pnpm-debug.log*
 10 | 
 11 | # Diagnostic reports (https://nodejs.org/api/report.html)
 12 | 
 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
 14 | 
 15 | # Runtime data
 16 | 
 17 | pids
 18 | _.pid
 19 | _.seed
 20 | \*.pid.lock
 21 | 
 22 | # Directory for instrumented libs generated by jscoverage/JSCover
 23 | 
 24 | lib-cov
 25 | 
 26 | # Coverage directory used by tools like istanbul
 27 | 
 28 | coverage
 29 | \*.lcov
 30 | 
 31 | # nyc test coverage
 32 | 
 33 | .nyc_output
 34 | 
 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 36 | 
 37 | .grunt
 38 | 
 39 | # Bower dependency directory (https://bower.io/)
 40 | 
 41 | bower_components
 42 | 
 43 | # node-waf configuration
 44 | 
 45 | .lock-wscript
 46 | 
 47 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 48 | 
 49 | build/Release
 50 | 
 51 | # Dependency directories
 52 | 
 53 | node_modules/
 54 | jspm_packages/
 55 | 
 56 | # Snowpack dependency directory (https://snowpack.dev/)
 57 | 
 58 | web_modules/
 59 | 
 60 | # TypeScript cache
 61 | 
 62 | \*.tsbuildinfo
 63 | 
 64 | # Optional npm cache directory
 65 | 
 66 | .npm
 67 | 
 68 | # Optional eslint cache
 69 | 
 70 | .eslintcache
 71 | 
 72 | # Optional stylelint cache
 73 | 
 74 | .stylelintcache
 75 | 
 76 | # Microbundle cache
 77 | 
 78 | .rpt2_cache/
 79 | .rts2_cache_cjs/
 80 | .rts2_cache_es/
 81 | .rts2_cache_umd/
 82 | 
 83 | # Optional REPL history
 84 | 
 85 | .node_repl_history
 86 | 
 87 | # Output of 'npm pack'
 88 | 
 89 | \*.tgz
 90 | 
 91 | # Yarn Integrity file
 92 | 
 93 | .yarn-integrity
 94 | 
 95 | # dotenv environment variable files
 96 | 
 97 | .env
 98 | .env.development.local
 99 | .env.test.local
100 | .env.production.local
101 | .env.local
102 | 
103 | # parcel-bundler cache (https://parceljs.org/)
104 | 
105 | .cache
106 | .parcel-cache
107 | 
108 | # Next.js build output
109 | 
110 | .next
111 | out
112 | 
113 | # Nuxt.js build / generate output
114 | 
115 | .nuxt
116 | dist
117 | 
118 | # Gatsby files
119 | 
120 | .cache/
121 | 
122 | # Comment in the public line in if your project uses Gatsby and not Next.js
123 | 
124 | # https://nextjs.org/blog/next-9-1#public-directory-support
125 | 
126 | # public
127 | 
128 | # vuepress build output
129 | 
130 | .vuepress/dist
131 | 
132 | # vuepress v2.x temp and cache directory
133 | 
134 | .temp
135 | .cache
136 | 
137 | # Docusaurus cache and generated files
138 | 
139 | .docusaurus
140 | 
141 | # Serverless directories
142 | 
143 | .serverless/
144 | 
145 | # FuseBox cache
146 | 
147 | .fusebox/
148 | 
149 | # DynamoDB Local files
150 | 
151 | .dynamodb/
152 | 
153 | # TernJS port file
154 | 
155 | .tern-port
156 | 
157 | # Stores VSCode versions used for testing VSCode extensions
158 | 
159 | .vscode-test
160 | 
161 | # yarn v2
162 | 
163 | .yarn/cache
164 | .yarn/unplugged
165 | .yarn/build-state.yml
166 | .yarn/install-state.gz
167 | .pnp.\*
168 | 
169 | # wrangler project
170 | 
171 | .dev.vars
172 | .wrangler/
173 | 


--------------------------------------------------------------------------------
/examples/servers/cf-agents/README.md:
--------------------------------------------------------------------------------
 1 | # Building a Remote MCP Server on Cloudflare (Without Auth)
 2 | 
 3 | This example allows you to deploy a remote MCP server that doesn't require authentication on Cloudflare Workers. 
 4 | 
 5 | ## Get started: 
 6 | 
 7 | [![Deploy to Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-authless)
 8 | 
 9 | This will deploy your MCP server to a URL like: `remote-mcp-server-authless.<your-account>.workers.dev/sse`
10 | 
11 | Alternatively, you can use the command line below to get the remote MCP Server created on your local machine:
12 | ```bash
13 | npm create cloudflare@latest -- my-mcp-server --template=cloudflare/ai/demos/remote-mcp-authless
14 | ```
15 | 
16 | ## Customizing your MCP Server
17 | 
18 | To add your own [tools](https://developers.cloudflare.com/agents/model-context-protocol/tools/) to the MCP server, define each tool inside the `init()` method of `src/index.ts` using `this.server.tool(...)`. 
19 | 
20 | ## Connect to Cloudflare AI Playground
21 | 
22 | You can connect to your MCP server from the Cloudflare AI Playground, which is a remote MCP client:
23 | 
24 | 1. Go to https://playground.ai.cloudflare.com/
25 | 2. Enter your deployed MCP server URL (`remote-mcp-server-authless.<your-account>.workers.dev/sse`)
26 | 3. You can now use your MCP tools directly from the playground!
27 | 
28 | ## Connect Claude Desktop to your MCP server
29 | 
30 | You can also connect to your remote MCP server from local MCP clients, by using the [mcp-remote proxy](https://www.npmjs.com/package/mcp-remote). 
31 | 
32 | To connect to your MCP server from Claude Desktop, follow [Anthropic's Quickstart](https://modelcontextprotocol.io/quickstart/user) and within Claude Desktop go to Settings > Developer > Edit Config.
33 | 
34 | Update with this configuration:
35 | 
36 | ```json
37 | {
38 |   "mcpServers": {
39 |     "calculator": {
40 |       "command": "npx",
41 |       "args": [
42 |         "mcp-remote",
43 |         "http://localhost:8787/sse"  // or remote-mcp-server-authless.your-account.workers.dev/sse
44 |       ]
45 |     }
46 |   }
47 | }
48 | ```
49 | 
50 | Restart Claude and you should see the tools become available. 
51 | 


--------------------------------------------------------------------------------
/examples/servers/cf-agents/biome.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
 3 |   "assist": {
 4 |     "actions": {
 5 |       "source": {
 6 |         "useSortedKeys": "on"
 7 |       }
 8 |     },
 9 |     "enabled": true
10 |   },
11 |   "files": {
12 |     "includes": ["!worker-configuration.d.ts"]
13 |   },
14 |   "formatter": {
15 |     "enabled": true,
16 |     "indentWidth": 4,
17 |     "lineWidth": 100
18 |   },
19 |   "linter": {
20 |     "enabled": true,
21 |     "rules": {
22 |       "recommended": true,
23 |       "style": {
24 |         "noInferrableTypes": "error",
25 |         "noNonNullAssertion": "off",
26 |         "noParameterAssign": "error",
27 |         "noUnusedTemplateLiteral": "error",
28 |         "noUselessElse": "error",
29 |         "useAsConstAssertion": "error",
30 |         "useDefaultParameterLast": "error",
31 |         "useEnumInitializers": "error",
32 |         "useNumberNamespace": "error",
33 |         "useSelfClosingElements": "error",
34 |         "useSingleVarDeclarator": "error"
35 |       },
36 |       "suspicious": {
37 |         "noConfusingVoidType": "off",
38 |         "noDebugger": "off",
39 |         "noExplicitAny": "off"
40 |       }
41 |     }
42 |   },
43 |   "root": false,
44 |   "vcs": {
45 |     "clientKind": "git",
46 |     "enabled": true,
47 |     "useIgnoreFile": true
48 |   }
49 | }
50 | 


--------------------------------------------------------------------------------
/examples/servers/cf-agents/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "remote-mcp-server-authless",
 3 |   "version": "0.0.0",
 4 |   "private": true,
 5 |   "scripts": {
 6 |     "deploy": "wrangler deploy",
 7 |     "dev": "wrangler dev --port 5102",
 8 |     "format": "biome format --write",
 9 |     "lint:fix": "biome lint --fix",
10 |     "start": "wrangler dev",
11 |     "cf-typegen": "wrangler types",
12 |     "type-check": "tsc --noEmit"
13 |   },
14 |   "dependencies": {
15 |     "@modelcontextprotocol/sdk": "^1.13.3",
16 |     "zod": "^3.25.67"
17 |   },
18 |   "devDependencies": {
19 |     "@biomejs/biome": "^2.0.4",
20 |     "@cloudflare/workers-oauth-provider": "^0.0.5",
21 |     "@types/node": "^24.0.7",
22 |     "agents": "^0.0.100",
23 |     "hono": "^4.8.3",
24 |     "typescript": "^5.8.3",
25 |     "wrangler": "^4.23.0"
26 |   }
27 | }
28 | 


--------------------------------------------------------------------------------
/examples/servers/cf-agents/src/index.ts:
--------------------------------------------------------------------------------
  1 | import { McpAgent } from 'agents/mcp'
  2 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
  3 | import { z } from 'zod'
  4 | import OAuthProvider, { type OAuthHelpers } from '@cloudflare/workers-oauth-provider'
  5 | import { Hono } from 'hono'
  6 | 
  7 | // Define our MCP agent with tools
  8 | export class MyMCP extends McpAgent {
  9 |   server = new McpServer({
 10 |     name: 'Authless Calculator',
 11 |     version: '1.0.0',
 12 |   })
 13 | 
 14 |   async init() {
 15 |     // Simple addition tool
 16 |     this.server.tool('add', { a: z.number(), b: z.number() }, async ({ a, b }) => ({
 17 |       content: [{ type: 'text', text: String(a + b) }],
 18 |     }))
 19 | 
 20 |     // Calculator tool with multiple operations
 21 |     this.server.tool(
 22 |       'calculate',
 23 |       {
 24 |         operation: z.enum(['add', 'subtract', 'multiply', 'divide']),
 25 |         a: z.number(),
 26 |         b: z.number(),
 27 |       },
 28 |       async ({ operation, a, b }) => {
 29 |         let result: number
 30 |         switch (operation) {
 31 |           case 'add':
 32 |             result = a + b
 33 |             break
 34 |           case 'subtract':
 35 |             result = a - b
 36 |             break
 37 |           case 'multiply':
 38 |             result = a * b
 39 |             break
 40 |           case 'divide':
 41 |             if (b === 0)
 42 |               return {
 43 |                 content: [
 44 |                   {
 45 |                     type: 'text',
 46 |                     text: 'Error: Cannot divide by zero',
 47 |                   },
 48 |                 ],
 49 |               }
 50 |             result = a / b
 51 |             break
 52 |         }
 53 |         return { content: [{ type: 'text', text: String(result) }] }
 54 |       },
 55 |     )
 56 | 
 57 |     // Register sample resources using the agents framework API
 58 |     this.server.resource('calc://history', 'Calculation History', async () => ({
 59 |       contents: [
 60 |         {
 61 |           uri: 'calc://history',
 62 |           mimeType: 'application/json',
 63 |           text: JSON.stringify(
 64 |             {
 65 |               calculations: [
 66 |                 { operation: 'add', a: 5, b: 3, result: 8, timestamp: '2024-01-01T10:00:00Z' },
 67 |                 { operation: 'multiply', a: 4, b: 7, result: 28, timestamp: '2024-01-01T10:01:00Z' },
 68 |               ],
 69 |             },
 70 |             null,
 71 |             2,
 72 |           ),
 73 |         },
 74 |       ],
 75 |     }))
 76 | 
 77 |     this.server.resource('calc://settings', 'Calculator Settings', async () => ({
 78 |       contents: [
 79 |         {
 80 |           uri: 'calc://settings',
 81 |           mimeType: 'application/json',
 82 |           text: JSON.stringify(
 83 |             {
 84 |               precision: 2,
 85 |               allowNegative: true,
 86 |               maxValue: 1000000,
 87 |             },
 88 |             null,
 89 |             2,
 90 |           ),
 91 |         },
 92 |       ],
 93 |     }))
 94 | 
 95 |     // Register a dynamic resource
 96 |     this.server.resource('calc://stats', 'Calculation Statistics', async () => ({
 97 |       contents: [
 98 |         {
 99 |           uri: 'calc://stats',
100 |           mimeType: 'application/json',
101 |           text: JSON.stringify(
102 |             {
103 |               totalCalculations: Math.floor(Math.random() * 100),
104 |               lastUpdated: new Date().toISOString(),
105 |             },
106 |             null,
107 |             2,
108 |           ),
109 |         },
110 |       ],
111 |     }))
112 | 
113 |     // Register sample prompts using the agents framework API
114 |     this.server.prompt(
115 |       'math_problem',
116 |       'Generate a math problem',
117 |       {
118 |         difficulty: z.string(),
119 |         topic: z.string().optional(),
120 |       },
121 |       async ({ difficulty, topic }) => ({
122 |         messages: [
123 |           {
124 |             role: 'user',
125 |             content: {
126 |               type: 'text',
127 |               text: `Generate a ${difficulty} math problem${topic ? ` about ${topic}` : ''}`,
128 |             },
129 |           },
130 |         ],
131 |       }),
132 |     )
133 | 
134 |     this.server.prompt('explain_calculation', 'Explain a calculation', { operation: z.string() }, async ({ operation }) => ({
135 |       messages: [
136 |         {
137 |           role: 'user',
138 |           content: {
139 |             type: 'text',
140 |             text: `Please explain how to perform ${operation} step by step with examples`,
141 |           },
142 |         },
143 |       ],
144 |     }))
145 |   }
146 | }
147 | 
148 | export type Bindings = Env & {
149 |   OAUTH_PROVIDER: OAuthHelpers
150 | }
151 | 
152 | const app = new Hono<{
153 |   Bindings: Bindings
154 | }>()
155 | 
156 | app.get('/authorize', async (c) => {
157 |   const oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw)
158 |   const email = 'example@dotcom.com'
159 |   const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({
160 |     request: oauthReqInfo,
161 |     userId: email,
162 |     metadata: {
163 |       label: 'Test User',
164 |     },
165 |     scope: oauthReqInfo.scope,
166 |     props: {
167 |       userEmail: email,
168 |     },
169 |   })
170 |   return Response.redirect(redirectTo)
171 | })
172 | 
173 | export default {
174 |   fetch(request: Request, env: Env, ctx: ExecutionContext) {
175 |     const url = new URL(request.url)
176 | 
177 |     if (url.pathname === '/public/sse' || url.pathname === '/public/sse/message') {
178 |       return MyMCP.serveSSE('/public/sse').fetch(request, env, ctx)
179 |     }
180 | 
181 |     if (url.pathname === '/public/mcp') {
182 |       return MyMCP.serve('/public/mcp').fetch(request, env, ctx)
183 |     }
184 | 
185 |     return new OAuthProvider({
186 |       apiRoute: ['/sse', '/mcp'],
187 |       apiHandler: {
188 |         // @ts-ignore
189 |         fetch: (request, env, ctx) => {
190 |           const { pathname } = new URL(request.url)
191 |           if (pathname.startsWith('/sse')) return MyMCP.serveSSE('/sse').fetch(request as any, env, ctx)
192 |           if (pathname === '/mcp') return MyMCP.serve('/mcp').fetch(request as any, env, ctx)
193 |           return new Response('Not found', { status: 404 })
194 |         },
195 |       },
196 |       // @ts-ignore
197 |       defaultHandler: app,
198 |       authorizeEndpoint: '/authorize',
199 |       tokenEndpoint: '/token',
200 |       clientRegistrationEndpoint: '/register',
201 |     }).fetch(request, env, ctx)
202 |   },
203 | }
204 | 


--------------------------------------------------------------------------------
/examples/servers/cf-agents/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "es2021",
 4 |     "lib": ["es2021"],
 5 |     "jsx": "react-jsx",
 6 |     "module": "es2022",
 7 |     "moduleResolution": "bundler",
 8 |     "resolveJsonModule": true,
 9 |     "allowJs": true,
10 |     "checkJs": false,
11 |     "noEmit": true,
12 |     "isolatedModules": true,
13 |     "allowSyntheticDefaultImports": true,
14 |     "forceConsistentCasingInFileNames": true,
15 |     "strict": true,
16 |     "skipLibCheck": true,
17 |     "types": ["./worker-configuration.d.ts", "node"]
18 |   },
19 |   "include": ["worker-configuration.d.ts", "src/**/*.ts"]
20 | }
21 | 


--------------------------------------------------------------------------------
/examples/servers/cf-agents/wrangler.jsonc:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * For more details on how to configure Wrangler, refer to:
 3 |  * https://developers.cloudflare.com/workers/wrangler/configuration/
 4 |  */
 5 | {
 6 |   "$schema": "node_modules/wrangler/config-schema.json",
 7 |   "name": "cf-agents",
 8 |   "main": "src/index.ts",
 9 |   "compatibility_date": "2025-03-10",
10 |   "compatibility_flags": ["nodejs_compat"],
11 |   "migrations": [
12 |     {
13 |       "new_sqlite_classes": ["MyMCP"],
14 |       "tag": "v1"
15 |     }
16 |   ],
17 |   "durable_objects": {
18 |     "bindings": [
19 |       {
20 |         "class_name": "MyMCP",
21 |         "name": "MCP_OBJECT"
22 |       }
23 |     ]
24 |   },
25 |   "observability": {
26 |     "enabled": true
27 |   },
28 |   "kv_namespaces": [
29 |     {
30 |       "binding": "OAUTH_KV",
31 |       "id": "<Add-KV-ID>"
32 |     }
33 |   ]
34 |   /**
35 |    * Smart Placement
36 |    * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
37 |    */
38 |   // "placement": { "mode": "smart" },
39 | 
40 |   /**
41 |    * Bindings
42 |    * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
43 |    * databases, object storage, AI inference, real-time communication and more.
44 |    * https://developers.cloudflare.com/workers/runtime-apis/bindings/
45 |    */
46 | 
47 |   /**
48 |    * Environment Variables
49 |    * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
50 |    */
51 |   // "vars": { "MY_VARIABLE": "production_value" },
52 |   /**
53 |    * Note: Use secrets to store sensitive data.
54 |    * https://developers.cloudflare.com/workers/configuration/secrets/
55 |    */
56 | 
57 |   /**
58 |    * Static Assets
59 |    * https://developers.cloudflare.com/workers/static-assets/binding/
60 |    */
61 |   // "assets": { "directory": "./public/", "binding": "ASSETS" },
62 | 
63 |   /**
64 |    * Service Bindings (communicate between multiple Workers)
65 |    * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
66 |    */
67 |   // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
68 | }
69 | 


--------------------------------------------------------------------------------
/examples/servers/hono-mcp/.gitignore:
--------------------------------------------------------------------------------
 1 | # prod
 2 | dist/
 3 | 
 4 | # dev
 5 | .yarn/
 6 | !.yarn/releases
 7 | .vscode/*
 8 | !.vscode/launch.json
 9 | !.vscode/*.code-snippets
10 | .idea/workspace.xml
11 | .idea/usage.statistics.xml
12 | .idea/shelf
13 | 
14 | # deps
15 | node_modules/
16 | .wrangler
17 | 
18 | # env
19 | .env
20 | .env.production
21 | .dev.vars
22 | 
23 | # logs
24 | logs/
25 | *.log
26 | npm-debug.log*
27 | yarn-debug.log*
28 | yarn-error.log*
29 | pnpm-debug.log*
30 | lerna-debug.log*
31 | 
32 | # misc
33 | .DS_Store
34 | 


--------------------------------------------------------------------------------
/examples/servers/hono-mcp/README.md:
--------------------------------------------------------------------------------
 1 | ```txt
 2 | npm install
 3 | npm run dev
 4 | ```
 5 | 
 6 | ```txt
 7 | npm run deploy
 8 | ```
 9 | 
10 | [For generating/synchronizing types based on your Worker configuration run](https://developers.cloudflare.com/workers/wrangler/commands/#types):
11 | 
12 | ```txt
13 | npm run cf-typegen
14 | ```
15 | 
16 | Pass the `CloudflareBindings` as generics when instantiation `Hono`:
17 | 
18 | ```ts
19 | // src/index.ts
20 | const app = new Hono<{ Bindings: CloudflareBindings }>()
21 | ```
22 | 


--------------------------------------------------------------------------------
/examples/servers/hono-mcp/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "vengabus",
 3 |   "scripts": {
 4 |     "dev": "wrangler dev --port 5101",
 5 |     "deploy": "wrangler deploy --minify",
 6 |     "cf-typegen": "wrangler types --env-interface CloudflareBindings"
 7 |   },
 8 |   "dependencies": {
 9 |     "hono": "^4.8.2"
10 |   },
11 |   "devDependencies": {
12 |     "@hono/mcp": "^0.1.0",
13 |     "@modelcontextprotocol/sdk": "^1.13.3",
14 |     "wrangler": "^4.23.0",
15 |     "zod": "^3.25.67"
16 |   }
17 | }
18 | 


--------------------------------------------------------------------------------
/examples/servers/hono-mcp/public/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/modelcontextprotocol/use-mcp/5b54994da722081c0dadd563741a9e99121a6733/examples/servers/hono-mcp/public/.gitkeep


--------------------------------------------------------------------------------
/examples/servers/hono-mcp/src/index.ts:
--------------------------------------------------------------------------------
  1 | import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
  2 | import { StreamableHTTPTransport } from '@hono/mcp'
  3 | import { Hono } from 'hono'
  4 | import { z } from 'zod'
  5 | import { cors } from 'hono/cors'
  6 | 
  7 | const app = new Hono()
  8 | 
  9 | app.use(
 10 |   '/*',
 11 |   cors({
 12 |     origin: '*',
 13 |     allowHeaders: ['content-type', 'mcp-session-id', 'mcp-protocol-version'],
 14 |     exposeHeaders: ['mcp-session-id'],
 15 |   }),
 16 | )
 17 | 
 18 | // Your MCP server implementation
 19 | const mcpServer = new McpServer({
 20 |   name: 'vengabus-mcp-server',
 21 |   version: '1.0.0',
 22 | })
 23 | 
 24 | mcpServer.registerTool(
 25 |   'calculate_party_capacity',
 26 |   {
 27 |     title: 'Party Bus Capacity Calculator',
 28 |     description: 'Calculate how many people can fit on the Vengabus',
 29 |     inputSchema: {
 30 |       busCount: z.number().describe('Number of Vengabuses'),
 31 |       peoplePerBus: z.number().describe('People per bus'),
 32 |     },
 33 |   },
 34 |   async ({ busCount, peoplePerBus }) => ({
 35 |     content: [
 36 |       {
 37 |         type: 'text',
 38 |         text: `🚌 ${busCount} Vengabuses can transport ${busCount * peoplePerBus} party people! The wheels of steel are turning! 🎉`,
 39 |       },
 40 |     ],
 41 |   }),
 42 | )
 43 | 
 44 | mcpServer.registerTool(
 45 |   'get_vengabus_times',
 46 |   {
 47 |     title: 'Vengabus Schedule Checker',
 48 |     description:
 49 |       "This checks to see when the next Vengabus is, or whether there is one for the user's current location. Please display all the information to the user in a tabulated form.",
 50 |     inputSchema: {},
 51 |   },
 52 |   async () => {
 53 |     // Simulate some async work
 54 |     await new Promise((resolve) => setTimeout(resolve, 100 + Math.random() * 100))
 55 |     return {
 56 |       content: [
 57 |         {
 58 |           type: 'text',
 59 |           text: `🚌 BOOM BOOM BOOM BOOM! Next Vengabus: IMMINENT! 🎉
 60 |           
 61 | Current Route: "Intercity Disco Express"
 62 | From: New York
 63 | To: IBIZA - THE MAGIC ISLAND! ✨
 64 | 
 65 | Schedule:
 66 | - Departure: When the beat drops
 67 | - Via: San Francisco (quick party stop)
 68 | - Arrival: When the sun comes up
 69 | 
 70 | Status: The party bus is jumping! 
 71 | Passengers: Maximum capacity!
 72 | BPM: 142 and rising!
 73 | 
 74 | We're going to Ibiza! Back to the island! 🏝️`,
 75 |         },
 76 |       ],
 77 |     }
 78 |   },
 79 | )
 80 | 
 81 | // Register sample resources
 82 | mcpServer.registerResource(
 83 |   'config',
 84 |   'config://vengabus',
 85 |   {
 86 |     title: 'Vengabus Fleet Configuration',
 87 |     description: 'Current Vengabus fleet settings and party parameters',
 88 |     mimeType: 'application/json',
 89 |   },
 90 |   async (uri: URL) => {
 91 |     const normalizedUri = uri.href.replace(/\/$/, '')
 92 |     return {
 93 |       contents: [
 94 |         {
 95 |           uri: normalizedUri,
 96 |           mimeType: 'application/json',
 97 |           text: JSON.stringify(
 98 |             {
 99 |               version: '1.0.0',
100 |               fleet: 'intercity-disco',
101 |               features: {
102 |                 bassBoost: true,
103 |                 strobeEnabled: true,
104 |                 maxBPM: 160,
105 |                 wheelsOfSteel: 'turning',
106 |               },
107 |               routes: ['New York to Ibiza', 'Back to the Magic Island', 'Like an Intercity Disco', 'The place to be'],
108 |             },
109 |             null,
110 |             2,
111 |           ),
112 |         },
113 |       ],
114 |     }
115 |   },
116 | )
117 | 
118 | mcpServer.registerResource(
119 |   'readme',
120 |   'docs://party-manual.md',
121 |   {
122 |     title: 'Vengabus Party Manual',
123 |     description: 'Official guide to the Vengabus experience',
124 |     mimeType: 'text/markdown',
125 |   },
126 |   () => ({
127 |     contents: [
128 |       {
129 |         uri: 'docs://party-manual.md',
130 |         mimeType: 'text/markdown',
131 |         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!*",
132 |       },
133 |     ],
134 |   }),
135 | )
136 | 
137 | // Register a resource template
138 | mcpServer.registerResource(
139 |   'stats',
140 |   new ResourceTemplate('party://stats/{metric}', { list: undefined }),
141 |   {
142 |     title: 'Vengabus Party Statistics',
143 |     description: 'Get party metrics (bpm, passengers, energy)',
144 |     mimeType: 'application/json',
145 |   },
146 |   async (uri: URL, { metric }) => {
147 |     return {
148 |       contents: [
149 |         {
150 |           uri: uri.href,
151 |           mimeType: 'application/json',
152 |           text: JSON.stringify(
153 |             {
154 |               metric: metric,
155 |               value:
156 |                 metric === 'bpm'
157 |                   ? 140 + Math.floor(Math.random() * 20)
158 |                   : metric === 'passengers'
159 |                     ? Math.floor(Math.random() * 50) + 20
160 |                     : metric === 'energy'
161 |                       ? Math.floor(Math.random() * 100)
162 |                       : 0,
163 |               unit:
164 |                 metric === 'bpm'
165 |                   ? 'beats per minute'
166 |                   : metric === 'passengers'
167 |                     ? 'party people'
168 |                     : metric === 'energy'
169 |                       ? 'party power %'
170 |                       : 'unknown',
171 |               status: 'The party is jumping!',
172 |               lastUpdated: new Date().toISOString(),
173 |             },
174 |             null,
175 |             2,
176 |           ),
177 |         },
178 |       ],
179 |     }
180 |   },
181 | )
182 | 
183 | // Register sample prompts
184 | mcpServer.registerPrompt(
185 |   'party_invitation',
186 |   {
187 |     title: 'Vengabus Party Invitation',
188 |     description: 'Generate a party invitation for the Vengabus',
189 |     argsSchema: {
190 |       name: z.string().describe('Name of the party person'),
191 |       destination: z.string().optional().describe('Where the Vengabus is heading'),
192 |     },
193 |   },
194 |   ({ name, destination }) => ({
195 |     messages: [
196 |       {
197 |         role: 'user',
198 |         content: {
199 |           type: 'text',
200 |           text: `Create a party invitation for ${name} to join the Vengabus${destination ? ` heading to ${destination}` : ''}`,
201 |         },
202 |       },
203 |     ],
204 |   }),
205 | )
206 | 
207 | mcpServer.registerPrompt(
208 |   'party_announcement',
209 |   {
210 |     title: 'Party Bus Announcement',
211 |     description: 'Generate party-themed announcements for various occasions',
212 |     argsSchema: {
213 |       event: z.string().describe('Type of event (deployment, release, milestone, etc.)'),
214 |       details: z.string().optional().describe('Additional details about the event'),
215 |     },
216 |   },
217 |   ({ event, details }) => ({
218 |     messages: [
219 |       {
220 |         role: 'user',
221 |         content: {
222 |           type: 'text',
223 |           text: `Create a party bus announcement for a ${event}${details ? `: ${details}` : ''}`,
224 |         },
225 |       },
226 |     ],
227 |   }),
228 | )
229 | 
230 | app.post('/mcp', async (c) => {
231 |   const transport = new StreamableHTTPTransport()
232 |   await mcpServer.connect(transport)
233 |   return transport.handleRequest(c)
234 | })
235 | 
236 | export default app
237 | 


--------------------------------------------------------------------------------
/examples/servers/hono-mcp/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ESNext",
 4 |     "module": "ESNext",
 5 |     "moduleResolution": "Bundler",
 6 |     "strict": true,
 7 |     "skipLibCheck": true,
 8 |     "lib": ["ESNext"],
 9 |     "jsx": "react-jsx",
10 |     "jsxImportSource": "hono/jsx",
11 |     "types": ["./worker-configuration.d.ts"]
12 |   }
13 | }
14 | 


--------------------------------------------------------------------------------
/examples/servers/hono-mcp/wrangler.jsonc:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * For more details on how to configure Wrangler, refer to:
 3 |  * https://developers.cloudflare.com/workers/wrangler/configuration/
 4 |  */
 5 | {
 6 |   "$schema": "node_modules/wrangler/config-schema.json",
 7 |   "name": "vengabus",
 8 |   "main": "src/index.ts",
 9 |   "compatibility_date": "2025-06-20",
10 |   "assets": {
11 |     "binding": "ASSETS",
12 |     "directory": "./public"
13 |   },
14 |   "observability": {
15 |     "enabled": true
16 |   }
17 |   /**
18 |    * Smart Placement
19 |    * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
20 |    */
21 |   // "placement": { "mode": "smart" },
22 | 
23 |   /**
24 |    * Bindings
25 |    * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
26 |    * databases, object storage, AI inference, real-time communication and more.
27 |    * https://developers.cloudflare.com/workers/runtime-apis/bindings/
28 |    */
29 | 
30 |   /**
31 |    * Environment Variables
32 |    * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
33 |    */
34 |   // "vars": { "MY_VARIABLE": "production_value" },
35 |   /**
36 |    * Note: Use secrets to store sensitive data.
37 |    * https://developers.cloudflare.com/workers/configuration/secrets/
38 |    */
39 | 
40 |   /**
41 |    * Static Assets
42 |    * https://developers.cloudflare.com/workers/static-assets/binding/
43 |    */
44 |   // "assets": { "directory": "./public/", "binding": "ASSETS" },
45 | 
46 |   /**
47 |    * Service Bindings (communicate between multiple Workers)
48 |    * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
49 |    */
50 |   // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
51 | }
52 | 


--------------------------------------------------------------------------------
/oranda.json:
--------------------------------------------------------------------------------
1 | {
2 |   "styles": {
3 |     "theme": "axo_light"
4 |   }
5 | }
6 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "use-mcp",
 3 |   "repository": "https://github.com/modelcontextprotocol/use-mcp",
 4 |   "version": "0.0.19",
 5 |   "type": "module",
 6 |   "files": [
 7 |     "dist",
 8 |     "README.md",
 9 |     "LICENSE"
10 |   ],
11 |   "exports": {
12 |     ".": {
13 |       "types": "./dist/index.d.ts",
14 |       "require": "./dist/index.js",
15 |       "import": "./dist/index.js"
16 |     },
17 |     "./react": {
18 |       "types": "./dist/react/index.d.ts",
19 |       "require": "./dist/react/index.js",
20 |       "import": "./dist/react/index.js"
21 |     }
22 |   },
23 |   "scripts": {
24 |     "install:all": "concurrently 'pnpm install' 'cd examples/chat-ui && pnpm install' 'cd examples/inspector && pnpm install' 'cd examples/servers/hono-mcp && pnpm install' 'cd examples/servers/cf-agents && pnpm install'",
25 |     "dev": "concurrently 'pnpm:build:watch' 'cd examples/chat-ui && pnpm dev' 'cd examples/inspector && pnpm dev' 'sleep 1 && cd examples/servers/hono-mcp && pnpm dev' 'sleep 2 && cd examples/servers/cf-agents && pnpm dev'",
26 |     "deploy:all": "concurrently 'pnpm build:site && pnpm deploy:site' 'cd examples/chat-ui && pnpm run deploy' 'cd examples/inspector && pnpm run deploy'",
27 |     "build:watch": "tsup --watch",
28 |     "build": "tsup",
29 |     "check": "prettier --check . && tsc",
30 |     "prettier:fix": "prettier --write .",
31 |     "fix:oranda": "sed -i 's/```tsx/```ts/g' README.md",
32 |     "build:site": "npx @axodotdev/oranda build",
33 |     "deploy:site": "npx wrangler deploy",
34 |     "prepare": "husky"
35 |   },
36 |   "dependencies": {
37 |     "@modelcontextprotocol/sdk": "^1.13.3",
38 |     "strict-url-sanitise": "^0.0.1"
39 |   },
40 |   "devDependencies": {
41 |     "@axodotdev/oranda": "^0.6.5",
42 |     "@types/react": "^19.0.12",
43 |     "concurrently": "^9.2.0",
44 |     "husky": "^9.1.7",
45 |     "prettier": "^3.5.3",
46 |     "react": "^19.0.0",
47 |     "tsup": "^8.4.0",
48 |     "tsx": "^4.19.3",
49 |     "typescript": "^5.8.2",
50 |     "wrangler": "^4.20.2"
51 |   },
52 |   "tsup": {
53 |     "entry": [
54 |       "src/index.ts",
55 |       "src/react/index.ts"
56 |     ],
57 |     "format": [
58 |       "esm"
59 |     ],
60 |     "dts": true,
61 |     "clean": true,
62 |     "outDir": "dist",
63 |     "external": [
64 |       "react",
65 |       "@modelcontextprotocol/sdk"
66 |     ]
67 |   },
68 |   "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
69 | }
70 | 


--------------------------------------------------------------------------------
/scripts/pre-commit:
--------------------------------------------------------------------------------
 1 | #!/bin/sh
 2 | 
 3 | # Pre-commit hook to run prettier on staged files
 4 | # This hook is called by "git commit" and formats only the files being committed.
 5 | 
 6 | echo "Running prettier on staged files..."
 7 | 
 8 | # Get list of staged files that prettier can handle
 9 | STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|jsx|ts|tsx|json|css|scss|md)
#39; | tr '\n' ' ')
10 | 
11 | if [ -z "$STAGED_FILES" ]; then
12 |     echo "No staged files need formatting."
13 |     exit 0
14 | fi
15 | 
16 | echo "Formatting files: $STAGED_FILES"
17 | 
18 | # Run prettier on staged files
19 | pnpm prettier --write $STAGED_FILES
20 | 
21 | # Add the formatted files back to staging
22 | git add $STAGED_FILES
23 | 
24 | echo "Prettier formatting completed."


--------------------------------------------------------------------------------
/src/auth/callback.ts:
--------------------------------------------------------------------------------
  1 | // callback.ts
  2 | import { auth } from '@modelcontextprotocol/sdk/client/auth.js'
  3 | import { BrowserOAuthClientProvider } from './browser-provider.js' // Adjust path
  4 | import { StoredState } from './types.js' // Adjust path, ensure definition includes providerOptions
  5 | 
  6 | /**
  7 |  * Handles the OAuth callback using the SDK's auth() function.
  8 |  * Assumes it's running on the page specified as the callbackUrl.
  9 |  */
 10 | export async function onMcpAuthorization() {
 11 |   const queryParams = new URLSearchParams(window.location.search)
 12 |   const code = queryParams.get('code')
 13 |   const state = queryParams.get('state')
 14 |   const error = queryParams.get('error')
 15 |   const errorDescription = queryParams.get('error_description')
 16 | 
 17 |   const logPrefix = '[mcp-callback]' // Generic prefix, or derive from stored state later
 18 |   console.log(`${logPrefix} Handling callback...`, { code, state, error, errorDescription })
 19 | 
 20 |   let provider: BrowserOAuthClientProvider | null = null
 21 |   let storedStateData: StoredState | null = null
 22 |   const stateKey = state ? `mcp:auth:state_${state}` : null // Reconstruct state key prefix assumption
 23 | 
 24 |   try {
 25 |     // --- Basic Error Handling ---
 26 |     if (error) {
 27 |       throw new Error(`OAuth error: ${error} - ${errorDescription || 'No description provided.'}`)
 28 |     }
 29 |     if (!code) {
 30 |       throw new Error('Authorization code not found in callback query parameters.')
 31 |     }
 32 |     if (!state || !stateKey) {
 33 |       throw new Error('State parameter not found or invalid in callback query parameters.')
 34 |     }
 35 | 
 36 |     // --- Retrieve Stored State & Provider Options ---
 37 |     const storedStateJSON = localStorage.getItem(stateKey)
 38 |     if (!storedStateJSON) {
 39 |       throw new Error(`Invalid or expired state parameter "${state}". No matching state found in storage.`)
 40 |     }
 41 |     try {
 42 |       storedStateData = JSON.parse(storedStateJSON) as StoredState
 43 |     } catch (e) {
 44 |       throw new Error('Failed to parse stored OAuth state.')
 45 |     }
 46 | 
 47 |     // Validate expiry
 48 |     if (!storedStateData.expiry || storedStateData.expiry < Date.now()) {
 49 |       localStorage.removeItem(stateKey) // Clean up expired state
 50 |       throw new Error('OAuth state has expired. Please try initiating authentication again.')
 51 |     }
 52 | 
 53 |     // Ensure provider options are present
 54 |     if (!storedStateData.providerOptions) {
 55 |       throw new Error('Stored state is missing required provider options.')
 56 |     }
 57 |     const { serverUrl, ...providerOptions } = storedStateData.providerOptions
 58 | 
 59 |     // --- Instantiate Provider ---
 60 |     console.log(`${logPrefix} Re-instantiating provider for server: ${serverUrl}`)
 61 |     provider = new BrowserOAuthClientProvider(serverUrl, providerOptions)
 62 | 
 63 |     // --- Call SDK Auth Function ---
 64 |     console.log(`${logPrefix} Calling SDK auth() to exchange code...`)
 65 |     // The SDK auth() function will internally:
 66 |     // 1. Use provider.clientInformation()
 67 |     // 2. Use provider.codeVerifier()
 68 |     // 3. Call exchangeAuthorization()
 69 |     // 4. Use provider.saveTokens() on success
 70 |     const authResult = await auth(provider, { serverUrl, authorizationCode: code })
 71 | 
 72 |     if (authResult === 'AUTHORIZED') {
 73 |       console.log(`${logPrefix} Authorization successful via SDK auth(). Notifying opener...`)
 74 |       // --- Notify Opener and Close (Success) ---
 75 |       if (window.opener && !window.opener.closed) {
 76 |         window.opener.postMessage({ type: 'mcp_auth_callback', success: true }, window.location.origin)
 77 |         window.close()
 78 |       } else {
 79 |         console.warn(`${logPrefix} No opener window detected. Redirecting to root.`)
 80 |         window.location.href = '/' // Or a configured post-auth destination
 81 |       }
 82 |       // Clean up state ONLY on success and after notifying opener
 83 |       localStorage.removeItem(stateKey)
 84 |     } else {
 85 |       // This case shouldn't happen if `authorizationCode` is provided to `auth()`
 86 |       console.warn(`${logPrefix} SDK auth() returned unexpected status: ${authResult}`)
 87 |       throw new Error(`Unexpected result from authentication library: ${authResult}`)
 88 |     }
 89 |   } catch (err) {
 90 |     console.error(`${logPrefix} Error during OAuth callback handling:`, err)
 91 |     const errorMessage = err instanceof Error ? err.message : String(err)
 92 | 
 93 |     // --- Notify Opener and Display Error (Failure) ---
 94 |     if (window.opener && !window.opener.closed) {
 95 |       window.opener.postMessage({ type: 'mcp_auth_callback', success: false, error: errorMessage }, window.location.origin)
 96 |       // Optionally close even on error, depending on UX preference
 97 |       // window.close();
 98 |     }
 99 | 
100 |     // Display error in the callback window
101 |     try {
102 |       document.body.innerHTML = `
103 |             <div style="font-family: sans-serif; padding: 20px;">
104 |             <h1>Authentication Error</h1>
105 |             <p style="color: red; background-color: #ffebeb; border: 1px solid red; padding: 10px; border-radius: 4px;">
106 |                 ${errorMessage}
107 |             </p>
108 |             <p>You can close this window or <a href="#" onclick="window.close(); return false;">click here to close</a>.</p>
109 |             <pre style="font-size: 0.8em; color: #555; margin-top: 20px; white-space: pre-wrap;">${
110 |               err instanceof Error ? err.stack : ''
111 |             }</pre>
112 |             </div>
113 |         `
114 |     } catch (displayError) {
115 |       console.error(`${logPrefix} Could not display error in callback window:`, displayError)
116 |     }
117 |     // Clean up potentially invalid state on error
118 |     if (stateKey) {
119 |       localStorage.removeItem(stateKey)
120 |     }
121 |     // Clean up potentially dangling verifier or last_auth_url if auth failed badly
122 |     // Note: saveTokens should clean these on success
123 |     if (provider) {
124 |       localStorage.removeItem(provider.getKey('code_verifier'))
125 |       localStorage.removeItem(provider.getKey('last_auth_url'))
126 |     }
127 |   }
128 | }
129 | 


--------------------------------------------------------------------------------
/src/auth/types.ts:
--------------------------------------------------------------------------------
 1 | import { OAuthMetadata } from '@modelcontextprotocol/sdk/shared/auth.js'
 2 | 
 3 | /**
 4 |  * Internal type for storing OAuth state in localStorage during the popup flow.
 5 |  * @internal
 6 |  */
 7 | export interface StoredState {
 8 |   expiry: number
 9 |   metadata?: OAuthMetadata // Optional: might not be needed if auth() rediscovers
10 |   serverUrlHash: string
11 |   // Add provider options needed on callback:
12 |   providerOptions: {
13 |     serverUrl: string
14 |     storageKeyPrefix: string
15 |     clientName: string
16 |     clientUri: string
17 |     callbackUrl: string
18 |   }
19 | }
20 | 


--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Core entry point for the use-mcp browser library.
 3 |  * Provides browser-specific, framework-agnostic components for MCP OAuth.
 4 |  */
 5 | 
 6 | export { BrowserOAuthClientProvider } from './auth/browser-provider.js'
 7 | export { onMcpAuthorization } from './auth/callback.js'
 8 | 
 9 | // Optionally re-export relevant types from SDK or internal types if needed
10 | export type { StoredState as InternalStoredState } from './auth/types.js' // Example if needed internally
11 | 
12 | // It's generally better to have users import SDK types directly from the SDK
13 | // export type { Tool, JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
14 | // export type { OAuthClientInformation, OAuthMetadata, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
15 | 


--------------------------------------------------------------------------------
/src/react/index.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Entry point for the use-mcp React integration.
 3 |  * Provides the useMcp hook and related types.
 4 |  */
 5 | 
 6 | export { useMcp } from './useMcp.js'
 7 | export type { UseMcpOptions, UseMcpResult } from './types.js'
 8 | 
 9 | // Re-export core types for convenience when using hook result
10 | export type { Tool, Resource, ResourceTemplate, Prompt } from '@modelcontextprotocol/sdk/types.js'
11 | 


--------------------------------------------------------------------------------
/src/react/types.ts:
--------------------------------------------------------------------------------
  1 | import { Tool, Resource, ResourceTemplate, Prompt } from '@modelcontextprotocol/sdk/types.js'
  2 | 
  3 | export type UseMcpOptions = {
  4 |   /** The /sse URL of your remote MCP server */
  5 |   url: string
  6 |   /** OAuth client name for registration (if dynamic registration is used) */
  7 |   clientName?: string
  8 |   /** OAuth client URI for registration (if dynamic registration is used) */
  9 |   clientUri?: string
 10 |   /** Custom callback URL for OAuth redirect (defaults to /oauth/callback on the current origin) */
 11 |   callbackUrl?: string
 12 |   /** Storage key prefix for OAuth data in localStorage (defaults to "mcp:auth") */
 13 |   storageKeyPrefix?: string
 14 |   /** Custom configuration for the MCP client identity */
 15 |   clientConfig?: {
 16 |     name?: string
 17 |     version?: string
 18 |   }
 19 |   /** Custom headers that can be used to bypass auth */
 20 |   customHeaders?: Record<string, string>
 21 |   /** Whether to enable verbose debug logging to the console and the log state */
 22 |   debug?: boolean
 23 |   /** Auto retry connection if initial connection fails, with delay in ms (default: false) */
 24 |   autoRetry?: boolean | number
 25 |   /** Auto reconnect if an established connection is lost, with delay in ms (default: 3000) */
 26 |   autoReconnect?: boolean | number
 27 |   /** Popup window features string (dimensions and behavior) for OAuth */
 28 |   popupFeatures?: string
 29 |   /** Transport type preference: 'auto' (HTTP with SSE fallback), 'http' (HTTP only), 'sse' (SSE only) */
 30 |   transportType?: 'auto' | 'http' | 'sse'
 31 |   /** Prevent automatic authentication popup on initial connection (default: false) */
 32 |   preventAutoAuth?: boolean
 33 | }
 34 | 
 35 | export type UseMcpResult = {
 36 |   /** List of tools available from the connected MCP server */
 37 |   tools: Tool[]
 38 |   /** List of resources available from the connected MCP server */
 39 |   resources: Resource[]
 40 |   /** List of resource templates available from the connected MCP server */
 41 |   resourceTemplates: ResourceTemplate[]
 42 |   /** List of prompts available from the connected MCP server */
 43 |   prompts: Prompt[]
 44 |   /**
 45 |    * The current state of the MCP connection:
 46 |    * - 'discovering': Checking server existence and capabilities (including auth requirements).
 47 |    * - 'pending_auth': Authentication is required but auto-popup was prevented. User action needed.
 48 |    * - 'authenticating': Authentication is required and the process (e.g., popup) has been initiated.
 49 |    * - 'connecting': Establishing the SSE connection to the server.
 50 |    * - 'loading': Connected; loading resources like the tool list.
 51 |    * - 'ready': Connected and ready for tool calls.
 52 |    * - 'failed': Connection or authentication failed. Check the `error` property.
 53 |    */
 54 |   state: 'discovering' | 'pending_auth' | 'authenticating' | 'connecting' | 'loading' | 'ready' | 'failed'
 55 |   /** If the state is 'failed', this provides the error message */
 56 |   error?: string
 57 |   /**
 58 |    * If authentication requires user interaction (e.g., popup was blocked),
 59 |    * this URL can be presented to the user to complete authentication manually in a new tab.
 60 |    */
 61 |   authUrl?: string
 62 |   /** Array of internal log messages (useful for debugging) */
 63 |   log: { level: 'debug' | 'info' | 'warn' | 'error'; message: string; timestamp: number }[]
 64 |   /**
 65 |    * Function to call a tool on the MCP server.
 66 |    * @param name The name of the tool to call.
 67 |    * @param args Optional arguments for the tool.
 68 |    * @returns A promise that resolves with the tool's result.
 69 |    * @throws If the client is not in the 'ready' state or the call fails.
 70 |    */
 71 |   callTool: (name: string, args?: Record<string, unknown>) => Promise<any>
 72 |   /**
 73 |    * Function to list resources from the MCP server.
 74 |    * @returns A promise that resolves when resources are refreshed.
 75 |    * @throws If the client is not in the 'ready' state.
 76 |    */
 77 |   listResources: () => Promise<void>
 78 |   /**
 79 |    * Function to read a resource from the MCP server.
 80 |    * @param uri The URI of the resource to read.
 81 |    * @returns A promise that resolves with the resource contents.
 82 |    * @throws If the client is not in the 'ready' state or the read fails.
 83 |    */
 84 |   readResource: (uri: string) => Promise<{ contents: Array<{ uri: string; mimeType?: string; text?: string; blob?: string }> }>
 85 |   /**
 86 |    * Function to list prompts from the MCP server.
 87 |    * @returns A promise that resolves when prompts are refreshed.
 88 |    * @throws If the client is not in the 'ready' state.
 89 |    */
 90 |   listPrompts: () => Promise<void>
 91 |   /**
 92 |    * Function to get a specific prompt from the MCP server.
 93 |    * @param name The name of the prompt to get.
 94 |    * @param args Optional arguments for the prompt.
 95 |    * @returns A promise that resolves with the prompt messages.
 96 |    * @throws If the client is not in the 'ready' state or the get fails.
 97 |    */
 98 |   getPrompt: (
 99 |     name: string,
100 |     args?: Record<string, string>,
101 |   ) => Promise<{ messages: Array<{ role: 'user' | 'assistant'; content: { type: string; text?: string; [key: string]: any } }> }>
102 |   /** Manually attempts to reconnect if the state is 'failed'. */
103 |   retry: () => void
104 |   /** Disconnects the client from the MCP server. */
105 |   disconnect: () => void
106 |   /**
107 |    * Manually triggers the authentication process. Useful if the initial attempt failed
108 |    * due to a blocked popup, allowing the user to initiate it via a button click.
109 |    * @returns A promise that resolves with the authorization URL opened (or intended to be opened),
110 |    *          or undefined if auth cannot be started.
111 |    */
112 |   authenticate: () => void
113 |   /** Clears all stored authentication data (tokens, client info, etc.) for this server URL from localStorage. */
114 |   clearStorage: () => void
115 | }
116 | 


--------------------------------------------------------------------------------
/src/utils/assert.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Asserts that a condition is true, throwing an error if not.
 3 |  * Useful for type narrowing.
 4 |  * @param condition The condition to check.
 5 |  * @param message The error message to throw if the condition is false.
 6 |  */
 7 | export function assert(condition: unknown, message: string): asserts condition {
 8 |   if (!condition) {
 9 |     throw new Error(message)
10 |   }
11 | }
12 | 


--------------------------------------------------------------------------------
/test/README.md:
--------------------------------------------------------------------------------
  1 | # Integration Test Suite
  2 | 
  3 | This directory contains integration tests for the use-mcp project that test the complete flow:
  4 | 
  5 | 1. Building the use-mcp library
  6 | 2. Building the inspector example 
  7 | 3. Starting MCP server(s)
  8 | 4. Testing browser connections to MCP servers
  9 | 
 10 | ## Setup
 11 | 
 12 | Install test dependencies:
 13 | 
 14 | ```bash
 15 | cd test
 16 | pnpm install
 17 | ```
 18 | 
 19 | ## Running Tests
 20 | 
 21 | Run all integration tests (shows browser window):
 22 | 
 23 | ```bash
 24 | cd test
 25 | pnpm test
 26 | ```
 27 | 
 28 | Run tests headlessly (no browser window):
 29 | 
 30 | ```bash
 31 | cd test
 32 | pnpm test:headless
 33 | ```
 34 | 
 35 | Run tests in watch mode (re-runs on file changes):
 36 | 
 37 | ```bash
 38 | cd test
 39 | pnpm test:watch
 40 | ```
 41 | 
 42 | Run tests with interactive UI:
 43 | 
 44 | ```bash
 45 | cd test  
 46 | pnpm test:ui
 47 | ```
 48 | 
 49 | Debug hanging processes:
 50 | 
 51 | ```bash
 52 | cd test
 53 | pnpm test:debug
 54 | ```
 55 | 
 56 | ## Test Architecture
 57 | 
 58 | - **Global Setup** (`setup/global-setup.ts`): 
 59 |   - Builds use-mcp library and inspector
 60 |   - Starts hono-mcp server on port 9901
 61 |   - Starts static file server for inspector dist files
 62 |   
 63 | - **Global Teardown** (`setup/global-teardown.ts`):
 64 |   - Stops all servers and cleans up resources
 65 | 
 66 | - **Integration Tests** (`integration/mcp-connection.test.ts`):
 67 |   - Uses Playwright to automate browser interactions
 68 |   - Tests connection to MCP servers defined in `MCP_SERVERS` array
 69 |   - Verifies successful connections and tool availability
 70 |   - Logs debug information on failures
 71 | 
 72 | ## Adding New MCP Servers
 73 | 
 74 | To test additional MCP servers, add them to the `MCP_SERVERS` array in the test file:
 75 | 
 76 | ```typescript
 77 | const MCP_SERVERS = [
 78 |   {
 79 |     name: 'hono-mcp',
 80 |     url: 'http://localhost:9901/mcp',
 81 |     expectedTools: 1,
 82 |   },
 83 |   {
 84 |     name: 'new-server',
 85 |     url: 'http://localhost:9902/mcp',
 86 |     expectedTools: 2,
 87 |   },
 88 | ]
 89 | ```
 90 | 
 91 | Make sure to also update the global setup to start the new server process.
 92 | 
 93 | ## Test Output
 94 | 
 95 | Successful connections will log:
 96 | - ✅ Connection success message
 97 | - 📋 List of available tools with count
 98 | 
 99 | Failed connections will log:
100 | - ❌ Failure message  
101 | - 🐛 Debug log from the inspector interface
102 | 
103 | ## Troubleshooting
104 | 
105 | If tests fail:
106 | 
107 | 1. Check that all required dependencies are installed
108 | 2. Verify that ports 9901 and 8000+ are available
109 | 3. Look at the debug log output for MCP connection errors
110 | 4. Check browser console logs for JavaScript errors
111 | 5. Run tests with `--headed` flag to see browser interactions
112 | 


--------------------------------------------------------------------------------
/test/integration/mcp-connection.test.ts:
--------------------------------------------------------------------------------
  1 | import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'
  2 | import { chromium, type Browser, type Page } from 'playwright'
  3 | import { SERVER_CONFIGS } from './server-configs.js'
  4 | import { getTestState, connectToMCPServer, cleanupProcess } from './test-utils.js'
  5 | 
  6 | describe('MCP Connection Integration Tests', () => {
  7 |   let browser: Browser
  8 |   let page: Page
  9 | 
 10 |   beforeAll(async () => {
 11 |     const headless = process.env.HEADLESS === 'true'
 12 |     console.log(`🌐 Launching browser in ${headless ? 'headless' : 'headed'} mode`)
 13 |     browser = await chromium.launch({ headless })
 14 |   }, 30000)
 15 | 
 16 |   afterAll(async () => {
 17 |     if (browser) {
 18 |       await browser.close()
 19 |     }
 20 | 
 21 |     // Force cleanup before Vitest exits
 22 |     const state = globalThis.__INTEGRATION_TEST_STATE__
 23 |     if (state?.honoServer) {
 24 |       cleanupProcess(state.honoServer, 'hono-mcp')
 25 |     }
 26 |     if (state?.cfAgentsServer) {
 27 |       cleanupProcess(state.cfAgentsServer, 'cf-agents')
 28 |     }
 29 |     if (state?.staticServer) {
 30 |       try {
 31 |         state.staticServer.close()
 32 |         state.staticServer.closeAllConnections?.()
 33 |       } catch (e) {
 34 |         // Ignore errors
 35 |       }
 36 |     }
 37 |   })
 38 | 
 39 |   beforeEach(async () => {
 40 |     const context = await browser.newContext()
 41 |     page = await context.newPage()
 42 | 
 43 |     // Enable console logging for debugging
 44 |     page.on('console', (msg) => {
 45 |       if (msg.type() === 'error') {
 46 |         console.error(`Browser error: ${msg.text()}`)
 47 |       }
 48 |     })
 49 |   })
 50 | 
 51 |   afterEach(async () => {
 52 |     if (page) {
 53 |       await page.close()
 54 |       await page.context().close()
 55 |     }
 56 |   })
 57 | 
 58 |   // Test each server configuration
 59 |   for (const serverConfig of SERVER_CONFIGS) {
 60 |     describe(`${serverConfig.name} server`, () => {
 61 |       // Test each endpoint for this server
 62 |       for (const endpoint of serverConfig.endpoints) {
 63 |         // Test each transport type for this endpoint
 64 |         for (const transportType of endpoint.transportTypes) {
 65 |           test(`should connect to ${endpoint.path} with ${transportType} transport`, async () => {
 66 |             const testState = getTestState()
 67 |             const port = testState[serverConfig.portKey]
 68 | 
 69 |             if (!port) {
 70 |               throw new Error(`Port not found for ${serverConfig.name} (${serverConfig.portKey})`)
 71 |             }
 72 | 
 73 |             const serverUrl = `http://localhost:${port}${endpoint.path}`
 74 |             console.log(`\n🔗 Testing connection to ${serverConfig.name} at ${serverUrl} with ${transportType} transport`)
 75 | 
 76 |             const result = await connectToMCPServer(page, serverUrl, transportType)
 77 | 
 78 |             if (result.success) {
 79 |               console.log(`✅ Successfully connected to ${serverConfig.name}`)
 80 |               console.log(`📋 Available tools (${result.tools.length}):`)
 81 |               result.tools.forEach((tool, index) => {
 82 |                 console.log(`   ${index + 1}. ${tool}`)
 83 |               })
 84 | 
 85 |               if (result.resources.length > 0) {
 86 |                 console.log(`📂 Available resources (${result.resources.length}):`)
 87 |                 result.resources.forEach((resource, index) => {
 88 |                   console.log(`   ${index + 1}. ${resource}`)
 89 |                 })
 90 |               }
 91 | 
 92 |               if (result.prompts.length > 0) {
 93 |                 console.log(`💬 Available prompts (${result.prompts.length}):`)
 94 |                 result.prompts.forEach((prompt, index) => {
 95 |                   console.log(`   ${index + 1}. ${prompt}`)
 96 |                 })
 97 |               }
 98 | 
 99 |               // Verify connection success
100 |               expect(result.success).toBe(true)
101 |               expect(result.tools.length).toBeGreaterThanOrEqual(serverConfig.expectedTools)
102 | 
103 |               // Verify resources if expected
104 |               if (serverConfig.expectedResources !== undefined) {
105 |                 expect(result.resources.length).toBeGreaterThanOrEqual(serverConfig.expectedResources)
106 |               }
107 | 
108 |               // Verify prompts if expected
109 |               if (serverConfig.expectedPrompts !== undefined) {
110 |                 expect(result.prompts.length).toBeGreaterThanOrEqual(serverConfig.expectedPrompts)
111 |               }
112 |             } else {
113 |               console.log(`❌ Failed to connect to ${serverConfig.name}`)
114 |               if (result.debugLog) {
115 |                 console.log(`🐛 Debug log:`)
116 |                 console.log(result.debugLog)
117 |               }
118 | 
119 |               // Check if this is an expected failure case
120 |               const isExpectedFailure = endpoint.path.endsWith('/sse') && transportType === 'auto'
121 | 
122 |               if (isExpectedFailure) {
123 |                 console.log(`ℹ️  Expected failure: SSE endpoint with auto transport`)
124 |                 expect(result.success).toBe(false)
125 |               } else {
126 |                 // Fail the test with detailed information
127 |                 throw new Error(
128 |                   `Expected to connect to ${serverConfig.name} with ${transportType} transport but failed. Debug log: ${result.debugLog}`,
129 |                 )
130 |               }
131 |             }
132 |           }, 45000)
133 |         }
134 |       }
135 |     })
136 |   }
137 | })
138 | 


--------------------------------------------------------------------------------
/test/integration/mcp-connection.test.ts.old:
--------------------------------------------------------------------------------
  1 | import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'
  2 | import { chromium, Browser, Page } from 'playwright'
  3 | import { SERVER_CONFIGS } from './server-configs.js'
  4 | import { getTestState, connectToMCPServer, cleanupProcess } from './test-utils.js'
  5 | 
  6 | describe('MCP Connection Integration Tests', () => {
  7 |   let browser: Browser
  8 |   let page: Page
  9 | 
 10 |   beforeAll(async () => {
 11 |     const headless = process.env.HEADLESS === 'true'
 12 |     console.log(`🌐 Launching browser in ${headless ? 'headless' : 'headed'} mode`)
 13 |     browser = await chromium.launch({ headless })
 14 |   }, 30000)
 15 | 
 16 |   afterAll(async () => {
 17 |     if (browser) {
 18 |       await browser.close()
 19 |     }
 20 | 
 21 |     // Force cleanup before Vitest exits
 22 |     const state = globalThis.__INTEGRATION_TEST_STATE__
 23 |     if (state?.honoServer) {
 24 |       cleanupProcess(state.honoServer, 'hono-mcp')
 25 |     }
 26 |     if (state?.cfAgentsServer) {
 27 |       cleanupProcess(state.cfAgentsServer, 'cf-agents')
 28 |     }
 29 |     if (state?.staticServer) {
 30 |       try {
 31 |         state.staticServer.close()
 32 |         state.staticServer.closeAllConnections?.()
 33 |       } catch (e) {
 34 |         // Ignore errors
 35 |       }
 36 |     }
 37 |   })
 38 | 
 39 |   beforeEach(async () => {
 40 |     const context = await browser.newContext()
 41 |     page = await context.newPage()
 42 | 
 43 |     // Enable console logging for debugging
 44 |     page.on('console', (msg) => {
 45 |       if (msg.type() === 'error') {
 46 |         console.error(`Browser error: ${msg.text()}`)
 47 |       }
 48 |     })
 49 |   })
 50 | 
 51 |   afterEach(async () => {
 52 |     if (page) {
 53 |       await page.close()
 54 |       await page.context().close()
 55 |     }
 56 |   })
 57 | 
 58 |   // Test each server configuration
 59 |   for (const serverConfig of SERVER_CONFIGS) {
 60 |     describe(`${serverConfig.name} server`, () => {
 61 |       // Test each endpoint for this server
 62 |       for (const endpoint of serverConfig.endpoints) {
 63 |         // Test each transport type for this endpoint
 64 |         for (const transportType of endpoint.transportTypes) {
 65 |           test(`should connect to ${endpoint.path} with ${transportType} transport`, async () => {
 66 |             const testState = getTestState()
 67 |             const port = testState[serverConfig.portKey]
 68 | 
 69 |             if (!port) {
 70 |               throw new Error(`Port not found for ${serverConfig.name} (${serverConfig.portKey})`)
 71 |             }
 72 | 
 73 |             const serverUrl = `http://localhost:${port}${endpoint.path}`
 74 |             console.log(`\n🔗 Testing connection to ${serverConfig.name} at ${serverUrl} with ${transportType} transport`)
 75 | 
 76 |             const result = await connectToMCPServer(page, serverUrl, transportType)
 77 | 
 78 |             if (result.success) {
 79 |               console.log(`✅ Successfully connected to ${serverConfig.name}`)
 80 |               console.log(`📋 Available tools (${result.tools.length}):`)
 81 |               result.tools.forEach((tool, index) => {
 82 |                 console.log(`   ${index + 1}. ${tool}`)
 83 |               })
 84 | 
 85 |               // Verify connection success
 86 |               expect(result.success).toBe(true)
 87 |               expect(result.tools.length).toBeGreaterThanOrEqual(serverConfig.expectedTools)
 88 |             } else {
 89 |               console.log(`❌ Failed to connect to ${serverConfig.name}`)
 90 |               if (result.debugLog) {
 91 |                 console.log(`🐛 Debug log:`)
 92 |                 console.log(result.debugLog)
 93 |               }
 94 | 
 95 |               // Check if this is an expected failure case
 96 |               const isExpectedFailure =
 97 |                 endpoint.path.endsWith('/sse') && transportType === 'auto'
 98 | 
 99 |               if (isExpectedFailure) {
100 |                 console.log(`ℹ️  Expected failure: SSE endpoint with auto transport`)
101 |                 expect(result.success).toBe(false)
102 |               } else {
103 |                 // Fail the test with detailed information
104 |                 throw new Error(
105 |                   `Expected to connect to ${serverConfig.name} with ${transportType} transport but failed. Debug log: ${result.debugLog}`
106 |                 )
107 |               }
108 |             }
109 |           }, 45000)
110 |         }
111 |       }
112 |     })
113 |   }
114 | })
115 | 


--------------------------------------------------------------------------------
/test/integration/server-configs.ts:
--------------------------------------------------------------------------------
 1 | import { join, dirname } from 'node:path'
 2 | import { fileURLToPath } from 'node:url'
 3 | import type { ServerConfig } from './test-utils.js'
 4 | 
 5 | const __dirname = dirname(fileURLToPath(import.meta.url))
 6 | const rootDir = join(__dirname, '../..')
 7 | 
 8 | /**
 9 |  * Configuration for all MCP servers to test
10 |  */
11 | export const SERVER_CONFIGS: ServerConfig[] = [
12 |   {
13 |     name: 'hono-mcp',
14 |     directory: join(rootDir, 'examples/servers/hono-mcp'),
15 |     portKey: 'honoPort',
16 |     endpoints: [
17 |       {
18 |         path: '/mcp',
19 |         transportTypes: ['auto', 'http'],
20 |       },
21 |     ],
22 |     expectedTools: 2,
23 |     expectedResources: 2, // 2 direct resources (templates are counted separately)
24 |     expectedPrompts: 2,
25 |   },
26 |   {
27 |     name: 'cf-agents',
28 |     directory: join(rootDir, 'examples/servers/cf-agents'),
29 |     portKey: 'cfAgentsPort',
30 |     endpoints: [
31 |       {
32 |         path: '/mcp',
33 |         transportTypes: ['auto', 'http'],
34 |       },
35 |       {
36 |         path: '/sse',
37 |         transportTypes: ['auto', 'sse'],
38 |       },
39 |       {
40 |         path: '/public/mcp',
41 |         transportTypes: ['auto', 'http'],
42 |       },
43 |       {
44 |         path: '/public/sse',
45 |         transportTypes: ['auto', 'sse'],
46 |       },
47 |     ],
48 |     expectedTools: 2,
49 |     expectedResources: 3, // 3 direct resources (no templates in cf-agents)
50 |     expectedPrompts: 2,
51 |   },
52 | ]
53 | 


--------------------------------------------------------------------------------
/test/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "use-mcp-integration-tests",
 3 |   "private": true,
 4 |   "type": "module",
 5 |   "scripts": {
 6 |     "test": "pnpm test:headless",
 7 |     "test:headed": "vitest --run",
 8 |     "test:headless": "HEADLESS=true vitest --run",
 9 |     "test:watch": "vitest",
10 |     "test:ui": "vitest --ui",
11 |     "test:debug": "VITEST_REPORTER=hanging-process vitest --run"
12 |   },
13 |   "dependencies": {
14 |     "playwright": "^1.49.0"
15 |   },
16 |   "devDependencies": {
17 |     "vitest": "^2.1.8",
18 |     "@vitest/ui": "^2.1.8",
19 |     "typescript": "^5.8.2",
20 |     "@types/node": "^20.17.10"
21 |   }
22 | }
23 | 


--------------------------------------------------------------------------------
/test/setup/global-setup.ts:
--------------------------------------------------------------------------------
  1 | import { spawn, ChildProcess } from 'child_process'
  2 | import { join, dirname } from 'path'
  3 | import { fileURLToPath } from 'url'
  4 | import { writeFileSync, mkdirSync, readFileSync } from 'fs'
  5 | import { createServer, Server } from 'http'
  6 | import { parse } from 'url'
  7 | import { extname } from 'path'
  8 | import { SERVER_CONFIGS } from '../integration/server-configs.js'
  9 | import { findAvailablePortFromBase, spawnDevServer, waitForOutput, TestState, cleanupProcess } from '../integration/test-utils.js'
 10 | 
 11 | const __dirname = dirname(fileURLToPath(import.meta.url))
 12 | const rootDir = join(__dirname, '../..')
 13 | const testDir = join(__dirname, '..')
 14 | const cacheDir = join(testDir, 'node_modules/.cache/use-mcp-tests')
 15 | const testStateFile = join(cacheDir, 'test-state.json')
 16 | 
 17 | interface GlobalState extends TestState {
 18 |   honoServer?: ChildProcess
 19 |   cfAgentsServer?: ChildProcess
 20 |   staticServer?: Server
 21 |   processGroupId?: number
 22 |   allChildProcesses?: Set<number>
 23 | }
 24 | 
 25 | declare global {
 26 |   var __INTEGRATION_TEST_STATE__: GlobalState
 27 | }
 28 | 
 29 | function runCommand(command: string, args: string[], cwd: string, env?: Record<string, string>): Promise<void> {
 30 |   return new Promise((resolve, reject) => {
 31 |     const child = spawn(command, args, {
 32 |       cwd,
 33 |       stdio: 'inherit',
 34 |       shell: true,
 35 |       env: env ? { ...process.env, ...env } : process.env,
 36 |     })
 37 | 
 38 |     child.on('close', (code) => {
 39 |       if (code === 0) {
 40 |         resolve()
 41 |       } else {
 42 |         reject(new Error(`Command failed with code ${code}: ${command} ${args.join(' ')}`))
 43 |       }
 44 |     })
 45 | 
 46 |     child.on('error', reject)
 47 |   })
 48 | }
 49 | 
 50 | function findAvailablePort(startPort = 8000): Promise<number> {
 51 |   return new Promise((resolve) => {
 52 |     const server = createServer()
 53 |     server
 54 |       .listen(startPort, () => {
 55 |         const port = (server.address() as any)?.port
 56 |         server.close(() => resolve(port))
 57 |       })
 58 |       .on('error', () => {
 59 |         resolve(findAvailablePort(startPort + 1))
 60 |       })
 61 |   })
 62 | }
 63 | 
 64 | export default async function globalSetup() {
 65 |   console.log('🔧 Setting up integration test environment...')
 66 | 
 67 |   const state: GlobalState = {
 68 |     allChildProcesses: new Set<number>(),
 69 |   }
 70 |   globalThis.__INTEGRATION_TEST_STATE__ = state
 71 | 
 72 |   // Set up signal handlers for cleanup
 73 |   const cleanup = () => {
 74 |     // Kill all tracked child processes
 75 |     if (state.allChildProcesses) {
 76 |       for (const pid of state.allChildProcesses) {
 77 |         try {
 78 |           process.kill(pid, 'SIGTERM')
 79 |           setTimeout(() => {
 80 |             try {
 81 |               process.kill(pid, 'SIGKILL')
 82 |             } catch (e) {
 83 |               // Process already dead, ignore
 84 |             }
 85 |           }, 1000)
 86 |         } catch (e) {
 87 |           // Process already dead, ignore
 88 |         }
 89 |       }
 90 |     }
 91 | 
 92 |     // Also try process group cleanup as backup
 93 |     if (state.processGroupId) {
 94 |       try {
 95 |         process.kill(-state.processGroupId, 'SIGTERM')
 96 |         setTimeout(() => {
 97 |           try {
 98 |             process.kill(-state.processGroupId!, 'SIGKILL')
 99 |           } catch (e) {
100 |             // Ignore errors
101 |           }
102 |         }, 1000)
103 |       } catch (e) {
104 |         // Ignore errors - process group may not exist
105 |       }
106 |     }
107 | 
108 |     if (state.staticServer) {
109 |       state.staticServer.close()
110 |       state.staticServer.closeAllConnections?.()
111 |     }
112 |   }
113 | 
114 |   process.on('SIGINT', cleanup)
115 |   process.on('SIGTERM', cleanup)
116 |   process.on('exit', cleanup)
117 | 
118 |   try {
119 |     // Step 1: Build use-mcp library
120 |     console.log('📦 Building use-mcp library...')
121 |     await runCommand('pnpm', ['build'], rootDir)
122 | 
123 |     // Step 2: Build inspector example
124 |     console.log('📦 Building inspector example...')
125 |     const inspectorDir = join(rootDir, 'examples/inspector')
126 |     await runCommand('pnpm', ['build'], inspectorDir, { NO_MINIFY: 'true' })
127 | 
128 |     // Step 3: Start all configured MCP servers
129 |     let basePort = Math.floor(10_000 + Math.random() * 500)
130 | 
131 |     for (const serverConfig of SERVER_CONFIGS) {
132 |       console.log(`🔍 Finding available port starting from ${basePort} for ${serverConfig.name}...`)
133 |       const port = await findAvailablePortFromBase(basePort)
134 |       console.log(`📍 Using port ${port} for ${serverConfig.name} server`)
135 | 
136 |       console.log(`🚀 Starting ${serverConfig.name} server...`)
137 |       const server = spawnDevServer(serverConfig.name, serverConfig.directory, port)
138 | 
139 |       // Track all child processes
140 |       if (server.pid) {
141 |         state.allChildProcesses!.add(server.pid)
142 |       }
143 | 
144 |       // Store the port and server reference
145 |       state[serverConfig.portKey] = port
146 |       if (serverConfig.name === 'hono-mcp') {
147 |         state.honoServer = server
148 |         state.processGroupId = server.pid
149 |       } else if (serverConfig.name === 'cf-agents') {
150 |         state.cfAgentsServer = server
151 |       }
152 | 
153 |       // Track when the process exits to remove it from our tracking
154 |       server.on('exit', () => {
155 |         if (server.pid) {
156 |           state.allChildProcesses!.delete(server.pid)
157 |         }
158 |       })
159 | 
160 |       // Wait for server to be ready
161 |       await waitForOutput(server, 'Ready on')
162 | 
163 |       basePort += 1 // Increment base port for next server
164 |     }
165 | 
166 |     // Step 5: Start simple static file server for inspector
167 |     console.log('🌐 Starting static file server for inspector...')
168 |     const inspectorDistDir = join(inspectorDir, 'dist')
169 |     const staticPort = await findAvailablePort(8000)
170 | 
171 |     const staticServer = createServer((req, res) => {
172 |       const pathname = parse(req.url || '').pathname || '/'
173 |       let filePath = join(inspectorDistDir, pathname === '/' ? 'index.html' : pathname)
174 | 
175 |       // Always disable keep-alive
176 |       res.setHeader('Connection', 'close')
177 | 
178 |       try {
179 |         const data = readFileSync(filePath)
180 |         const ext = extname(filePath)
181 |         const contentType =
182 |           {
183 |             '.html': 'text/html',
184 |             '.js': 'application/javascript',
185 |             '.css': 'text/css',
186 |             '.json': 'application/json',
187 |             '.png': 'image/png',
188 |             '.jpg': 'image/jpeg',
189 |             '.gif': 'image/gif',
190 |             '.svg': 'image/svg+xml',
191 |           }[ext] || 'text/plain'
192 | 
193 |         res.writeHead(200, { 'Content-Type': contentType })
194 |         res.end(data)
195 |       } catch (e) {
196 |         // Fallback to index.html for SPA routing
197 |         try {
198 |           const indexData = readFileSync(join(inspectorDistDir, 'index.html'))
199 |           res.writeHead(200, { 'Content-Type': 'text/html' })
200 |           res.end(indexData)
201 |         } catch (e2) {
202 |           res.writeHead(404, { 'Content-Type': 'text/plain' })
203 |           res.end('Not Found')
204 |         }
205 |       }
206 |     })
207 | 
208 |     // Configure for immediate shutdown
209 |     staticServer.keepAliveTimeout = 0
210 |     staticServer.headersTimeout = 1
211 | 
212 |     await new Promise<void>((resolve) => {
213 |       staticServer.listen(staticPort, () => {
214 |         console.log(`📁 Static server running on http://localhost:${staticPort}`)
215 |         resolve()
216 |       })
217 |     })
218 | 
219 |     state.staticServer = staticServer
220 |     state.staticPort = staticPort
221 | 
222 |     // Write state to file for tests to read
223 |     mkdirSync(cacheDir, { recursive: true })
224 |     const testState: TestState = {
225 |       staticPort: state.staticPort,
226 |     }
227 | 
228 |     // Add port information for each configured server
229 |     for (const serverConfig of SERVER_CONFIGS) {
230 |       testState[serverConfig.portKey] = state[serverConfig.portKey]
231 |     }
232 | 
233 |     writeFileSync(testStateFile, JSON.stringify(testState, null, 2))
234 | 
235 |     console.log('✅ Integration test environment ready!')
236 |   } catch (error) {
237 |     console.error('❌ Failed to set up integration test environment:', error)
238 | 
239 |     // Cleanup on failure
240 |     if (state.honoServer) {
241 |       cleanupProcess(state.honoServer, 'hono-mcp')
242 |     }
243 |     if (state.cfAgentsServer) {
244 |       cleanupProcess(state.cfAgentsServer, 'cf-agents')
245 |     }
246 |     if (state.staticServer) {
247 |       state.staticServer.close()
248 |     }
249 | 
250 |     throw error
251 |   }
252 | }
253 | 


--------------------------------------------------------------------------------
/test/setup/global-teardown.ts:
--------------------------------------------------------------------------------
  1 | function isProcessRunning(pid: number): boolean {
  2 |   try {
  3 |     // Sending signal 0 checks if process exists without actually sending a signal
  4 |     process.kill(pid, 0)
  5 |     return true
  6 |   } catch (e) {
  7 |     return false
  8 |   }
  9 | }
 10 | 
 11 | async function findChildProcesses(parentPid: number): Promise<number[]> {
 12 |   return new Promise((resolve) => {
 13 |     const { spawn } = require('child_process')
 14 |     // Use ps to find all child processes
 15 |     const ps = spawn('ps', ['-o', 'pid,ppid', '-ax'])
 16 | 
 17 |     let output = ''
 18 |     ps.stdout?.on('data', (data: Buffer) => {
 19 |       output += data.toString()
 20 |     })
 21 | 
 22 |     ps.on('close', () => {
 23 |       const lines = output.split('\n')
 24 |       const children: number[] = []
 25 | 
 26 |       for (const line of lines) {
 27 |         const match = line.trim().match(/^(\d+)\s+(\d+)/)
 28 |         if (match) {
 29 |           const pid = parseInt(match[1])
 30 |           const ppid = parseInt(match[2])
 31 |           if (ppid === parentPid) {
 32 |             children.push(pid)
 33 |           }
 34 |         }
 35 |       }
 36 | 
 37 |       resolve(children)
 38 |     })
 39 | 
 40 |     ps.on('error', () => {
 41 |       resolve([])
 42 |     })
 43 |   })
 44 | }
 45 | 
 46 | async function killProcessSafely(pid: number, name: string = 'process'): Promise<void> {
 47 |   if (!isProcessRunning(pid)) {
 48 |     console.log(`✅ ${name} (${pid}) already stopped`)
 49 |     return
 50 |   }
 51 | 
 52 |   try {
 53 |     console.log(`🛑 Sending SIGTERM to ${name} (${pid})`)
 54 |     process.kill(pid, 'SIGTERM')
 55 | 
 56 |     // Wait up to 3 seconds for graceful shutdown
 57 |     for (let i = 0; i < 30; i++) {
 58 |       await new Promise((resolve) => setTimeout(resolve, 100))
 59 |       if (!isProcessRunning(pid)) {
 60 |         console.log(`✅ ${name} (${pid}) gracefully stopped`)
 61 |         return
 62 |       }
 63 |     }
 64 | 
 65 |     // Force kill if still running
 66 |     if (isProcessRunning(pid)) {
 67 |       console.log(`⚡ Force killing ${name} (${pid})`)
 68 |       process.kill(pid, 'SIGKILL')
 69 | 
 70 |       // Give it a moment to die
 71 |       await new Promise((resolve) => setTimeout(resolve, 100))
 72 | 
 73 |       if (isProcessRunning(pid)) {
 74 |         console.warn(`⚠️ ${name} (${pid}) still running after SIGKILL`)
 75 |       } else {
 76 |         console.log(`✅ ${name} (${pid}) force killed`)
 77 |       }
 78 |     }
 79 |   } catch (e: any) {
 80 |     if (e.code === 'ESRCH') {
 81 |       console.log(`✅ ${name} (${pid}) already stopped`)
 82 |     } else {
 83 |       console.warn(`Error killing ${name} (${pid}):`, e.message)
 84 |     }
 85 |   }
 86 | }
 87 | 
 88 | export default async function globalTeardown() {
 89 |   console.log('🧹 Cleaning up integration test environment...')
 90 | 
 91 |   const state = globalThis.__INTEGRATION_TEST_STATE__
 92 | 
 93 |   // Kill all tracked child processes first
 94 |   if (state?.allChildProcesses && state.allChildProcesses.size > 0) {
 95 |     console.log(`🛑 Cleaning up ${state.allChildProcesses.size} tracked child processes...`)
 96 | 
 97 |     // Find all child processes of our tracked processes too
 98 |     const allPidsToKill = new Set<number>()
 99 | 
100 |     for (const pid of state.allChildProcesses) {
101 |       allPidsToKill.add(pid)
102 | 
103 |       // Find children of this process
104 |       const children = await findChildProcesses(pid)
105 |       for (const childPid of children) {
106 |         allPidsToKill.add(childPid)
107 |       }
108 |     }
109 | 
110 |     console.log(`🛑 Found ${allPidsToKill.size} total processes to clean up (including children)`)
111 | 
112 |     const killPromises = Array.from(allPidsToKill).map((pid) => killProcessSafely(pid, 'process'))
113 | 
114 |     await Promise.all(killPromises)
115 |   }
116 | 
117 |   // Also clean up by process group as backup
118 |   if (state?.processGroupId && isProcessRunning(state.processGroupId)) {
119 |     console.log('🛑 Cleaning up process group as backup...')
120 | 
121 |     try {
122 |       // Send SIGTERM to the entire process group first
123 |       console.log(`💀 Sending SIGTERM to process group ${state.processGroupId}`)
124 |       process.kill(-state.processGroupId, 'SIGTERM')
125 | 
126 |       // Wait a bit for graceful shutdown
127 |       await new Promise((resolve) => setTimeout(resolve, 1000))
128 | 
129 |       // Check if any processes in the group are still running and force kill if needed
130 |       try {
131 |         process.kill(-state.processGroupId, 0) // Check if group exists
132 |         console.log(`⚡ Sending SIGKILL to process group ${state.processGroupId}`)
133 |         process.kill(-state.processGroupId, 'SIGKILL')
134 |       } catch (e: any) {
135 |         if (e.code !== 'ESRCH') {
136 |           console.warn('Error force-killing process group:', e.message)
137 |         }
138 |       }
139 |     } catch (e: any) {
140 |       if (e.code !== 'ESRCH') {
141 |         console.warn('Error terminating process group:', e.message)
142 |       }
143 |     }
144 |   }
145 | 
146 |   if (state?.staticServer) {
147 |     console.log('🛑 Stopping static file server...')
148 |     await new Promise<void>((resolve) => {
149 |       state.staticServer?.close((err) => {
150 |         if (err) {
151 |           console.warn('Warning closing static server:', err.message)
152 |         }
153 |         resolve()
154 |       })
155 |     })
156 | 
157 |     // Force close all keep-alive connections
158 |     state.staticServer?.closeAllConnections?.()
159 |   }
160 | 
161 |   // Clear references
162 |   if (state) {
163 |     state.honoServer = undefined
164 |     state.staticServer = undefined
165 |     state.processGroupId = undefined
166 |   }
167 | 
168 |   // Force garbage collection if available
169 |   if (global.gc) {
170 |     global.gc()
171 |   }
172 | 
173 |   console.log('✅ Cleanup complete!')
174 | }
175 | 


--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "ESNext",
 5 |     "moduleResolution": "bundler",
 6 |     "lib": ["ES2022", "DOM"],
 7 |     "strict": true,
 8 |     "esModuleInterop": true,
 9 |     "skipLibCheck": true,
10 |     "forceConsistentCasingInFileNames": true,
11 |     "resolveJsonModule": true,
12 |     "isolatedModules": true,
13 |     "noEmit": true,
14 |     "types": ["vitest/globals", "node"]
15 |   },
16 |   "include": ["**/*.ts", "**/*.js"]
17 | }
18 | 


--------------------------------------------------------------------------------
/test/vitest.config.ts:
--------------------------------------------------------------------------------
 1 | import { defineConfig } from 'vitest/config'
 2 | 
 3 | export default defineConfig({
 4 |   test: {
 5 |     globalSetup: ['./setup/global-setup.ts'],
 6 |     globalTeardown: ['./setup/global-teardown.ts'],
 7 |     testTimeout: 60000, // 60 second timeout for integration tests
 8 |     reporters: process.env.VITEST_REPORTER === 'hanging-process' ? ['hanging-process'] : ['default'],
 9 |   },
10 | })
11 | 


--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "ES2022",
 5 |     "moduleResolution": "bundler",
 6 |     "strict": true,
 7 |     "esModuleInterop": true,
 8 |     "noEmit": true,
 9 |     "lib": ["ES2022", "DOM"],
10 |     "types": ["react"],
11 |     "forceConsistentCasingInFileNames": true,
12 |     "resolveJsonModule": true
13 |   },
14 |   "exclude": ["examples", "test"]
15 | }
16 | 


--------------------------------------------------------------------------------
/wrangler.jsonc:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * For more details on how to configure Wrangler, refer to:
 3 |  * https://developers.cloudflare.com/workers/wrangler/configuration/
 4 |  */
 5 | {
 6 |   "$schema": "node_modules/wrangler/config-schema.json",
 7 |   "name": "use-mcp",
 8 |   "routes": [
 9 |     {
10 |       "pattern": "use-mcp.dev",
11 |       "custom_domain": true
12 |     }
13 |   ],
14 |   "workers_dev": true,
15 |   "compatibility_date": "2025-02-07",
16 |   "assets": {
17 |     "not_found_handling": "single-page-application",
18 |     "directory": "public"
19 |   },
20 |   "observability": {
21 |     "enabled": true
22 |   }
23 | }
24 | 


--------------------------------------------------------------------------------