Compare commits

..
Author SHA1 Message Date
Glen Maddern
40c75b1952 Claude Code
> When the assistant responds with a longer message including markdown markup, the UI renders it out but it ends up looking too spaced out. Can you investigate what library we're using to render the
  mark down and whether it's possible to tweak the visual spacing to keep things a little bit tighter. I'm thinking in particular headings and lists as well as the overall line height is a touch too
  big.

⏺ I'll investigate the markdown rendering library and spacing issues in your chat UI. Let me first explore the codebase to understand how markdown is being rendered.

⏺ Update Todos
  ⎿  ☐ Find and identify the markdown rendering library being used
     ☐ Analyze current CSS styling for markdown elements
     ☐ Adjust spacing for headings, lists, and line height

⏺ Search(pattern: "markdown|marked|remark|react-markdown", include: "*.json")
  ⎿  Found 1 file (ctrl+r to expand)

⏺ Search(pattern: "markdown|Markdown", include: "*.{js,jsx,ts,tsx}")
  ⎿  Found 2 files (ctrl+r to expand)

⏺ Search(pattern: "package.json")
  ⎿  Found 1 file (ctrl+r to expand)

⏺ Read(package.json)
  ⎿  Read 57 lines (ctrl+r to expand)

⏺ Read(src/components/messages/ChatMessage.tsx)
  ⎿  Read 54 lines (ctrl+r to expand)

⏺ Read(src/components/messages/AssistantMessage.tsx)
  ⎿  Read 32 lines (ctrl+r to expand)

⏺ Update Todos
  ⎿  ☒ Find and identify the markdown rendering library being used
     ☐ Analyze current CSS styling for markdown elements
     ☐ Adjust spacing for headings, lists, and line height

⏺ Now let me check the Tailwind typography configuration and look for any custom CSS:

⏺ Search(pattern: "**/*.css")
  ⎿  Found 3 files (ctrl+r to expand)

⏺ Search(pattern: "tailwind.config.*")
  ⎿  Found 1 file (ctrl+r to expand)

⏺ Search(pattern: "prose", include: "*.{js,jsx,ts,tsx,css}")
  ⎿  Found 2 files (ctrl+r to expand)

⏺ Read(tailwind.config.js)
  ⎿  Read 20 lines (ctrl+r to expand)

⏺ Read(src/index.css)
  ⎿  Read 32 lines (ctrl+r to expand)

⏺ Read(src/styles/github.css)
  ⎿  Read 120 lines (ctrl+r to expand)

⏺ Update Todos
  ⎿  ☒ Find and identify the markdown rendering library being used
     ☒ Analyze current CSS styling for markdown elements
     ☐ Adjust spacing for headings, lists, and line height

⏺ Perfect! I found the issue. You're using react-markdown with @tailwindcss/typography plugin (the prose classes). The spacing is controlled by Tailwind's typography plugin. Let me add custom CSS
  overrides to tighten the spacing:

⏺ Update(tailwind.config.js)
  ⎿  Updated tailwind.config.js with 45 additions
       11                  border: '0',
       12                  backgroundColor: 'transparent',
       13                },
       14 +              lineHeight: '1.5',
       15 +              h1: {
       16 +                marginTop: '1rem',
       17 +                marginBottom: '0.5rem',
       18 +              },
       19 +              h2: {
       20 +                marginTop: '1rem',
       21 +                marginBottom: '0.5rem',
       22 +              },
       23 +              h3: {
       24 +                marginTop: '0.75rem',
       25 +                marginBottom: '0.5rem',
       26 +              },
       27 +              h4: {
       28 +                marginTop: '0.75rem',
       29 +                marginBottom: '0.5rem',
       30 +              },
       31 +              h5: {
       32 +                marginTop: '0.75rem',
       33 +                marginBottom: '0.5rem',
       34 +              },
       35 +              h6: {
       36 +                marginTop: '0.75rem',
       37 +                marginBottom: '0.5rem',
       38 +              },
       39 +              p: {
       40 +                marginTop: '0.5rem',
       41 +                marginBottom: '0.5rem',
       42 +              },
       43 +              ul: {
       44 +                marginTop: '0.5rem',
       45 +                marginBottom: '0.5rem',
       46 +              },
       47 +              ol: {
       48 +                marginTop: '0.5rem',
       49 +                marginBottom: '0.5rem',
       50 +              },
       51 +              li: {
       52 +                marginTop: '0.25rem',
       53 +                marginBottom: '0.25rem',
       54 +              },
       55 +              blockquote: {
       56 +                marginTop: '0.5rem',
       57 +                marginBottom: '0.5rem',
       58 +              },
       59              },
       60            },
       61          },

⏺ Update Todos
  ⎿  ☒ Find and identify the markdown rendering library being used
     ☒ Analyze current CSS styling for markdown elements
     ☒ Adjust spacing for headings, lists, and line height

⏺ Fixed! You're using react-markdown with Tailwind's @tailwindcss/typography plugin. I've updated tailwind.config.js:14-58 to tighten the spacing by:

  - Reducing line height from default 1.75 to 1.5
  - Cutting heading margins (top: 1rem, bottom: 0.5rem for h1/h2, smaller for h3-h6)
  - Reducing paragraph and list margins to 0.5rem
  - Shrinking list item margins to 0.25rem

  The changes will take effect after restarting your dev server.

Total cost:            $0.1342
Total duration (API):  1m 7.4s
Total duration (wall): 5m 14.3s
Total code changes:    45 lines added, 0 lines removed
Token usage by model:
    claude-3-5-haiku:  638 input, 24 output, 0 cache read, 0 cache write
       claude-sonnet:  227 input, 2.5k output, 166.5k cache read, 12.1k cache write
2025-06-27 12:05:30 +10:00
Glen Maddern
c834b114a7 Round corners more on blue connected server transport type badge
Change from rounded-md to rounded-lg for better visual consistency
2025-06-26 09:50:50 +10:00
Glen Maddern
59d2dfca8b Move transport toggle to left and make it only affect new servers
- Add newServerTransportType state for next server to be added
- Move toggle to left side directly after 'Add New Server' label
- Add transportType to McpServer interface to store per-server transport
- Update connections to use server-specific transport type
- Change toggle styling to rounded-full (pill style)
2025-06-26 09:50:30 +10:00
Glen Maddern
4e4d9d99a3 Default transport type to HTTP instead of auto 2025-06-26 09:49:14 +10:00
Glen Maddern
39043c12d6 fin 2025-06-25 23:37:23 +10:00
Glen Maddern
0d0f30f4aa Almost ready 2025-06-25 22:14:36 +10:00
Glen Maddern
f460842bbc Ok _much_ better timing and just me understanding this stuff 2025-06-25 21:52:21 +10:00
Glen Maddern
cd8df15e94 Only updating one conversation at a time 2025-06-25 20:45:44 +10:00
Glen Maddern
b07b0a73e1 visual tweaks 2025-06-25 20:38:05 +10:00
Glen Maddern
9b34461c2d tool responses are easy now too 2025-06-25 20:21:25 +10:00
Glen Maddern
5e41222c63 Handling reasoning then messages at least 2025-06-25 20:20:14 +10:00
Glen Maddern
0c7b9e0537 Ok turns out I can be dumb too 2025-06-25 19:45:53 +10:00
Glen Maddern
caed6d373c Starting to realise how dumb the vibecoded UI was oops 2025-06-25 19:42:07 +10:00
Glen Maddern
ba845e7852 Big sigh, finally getting control of my markup 2025-06-25 19:01:30 +10:00
Glen Maddern
52c3e8d727 wip, at least prettier likes it 2025-06-25 17:13:45 +10:00
Glen Maddern
022747ba04 wip, changing the logic in useStreamResponse 2025-06-25 17:06:50 +10:00
Glen Maddern
6429f48763 lfg manual style 2025-06-25 15:12:03 +10:00
Glen Maddern
5e69116f47 fixing prettier 2025-06-25 15:09:09 +10:00
Glen Maddern
51fafb485c Branching off 2025-06-25 15:06:17 +10:00
Glen Maddern
ab73226b87 Remove all duplicate detection logic
- Remove existingToolCall and existingToolResult checks
- Add every API event as a new message regardless of duplicates
- Provides complete unfiltered log of all API data after middleware
- Useful for debugging and understanding raw data flow
2025-06-25 12:09:31 +10:00
Glen Maddern
840fbb4ad6 Simplify debug logging to show raw JSON messages
- Replace complex message summaries with JSON.stringify(conv.messages)
- Provides direct, unfiltered view of message data structure
- Easier to inspect actual data and debug issues
2025-06-25 12:06:57 +10:00
Glen Maddern
12dc6433c3 Enhance debug logging to show complete conversation state
- Show entire messages array after each new message addition
- Include message index, role, content/reasoning status, and tool info
- Add final conversation state with content/reasoning lengths
- Provides complete visibility into data structure evolution
2025-06-25 12:05:00 +10:00
Glen Maddern
4e282ea1de Add targeted debug logging for message operations
- Add debugMessages helper function that logs with [Messages] prefix
- Log when new messages are added to conversation (not updates)
- Log reasoning assistant messages, tool calls, tool results, and content messages
- Log stream processing completion and function completion
- Debug logging enabled with localStorage.setItem('USE_MCP_DEBUG', 'true')
2025-06-25 12:02:43 +10:00
Glen Maddern
2c8391007c Clean up debug logging in useStreamResponse.ts
- Remove all debugLog calls and debug infrastructure
- Simplify error logging by removing verbose prefixes
- Fix unused options parameter to eliminate warnings
- Clean up formatting and extra blank lines
2025-06-25 11:56:27 +10:00
Glen Maddern
4f87d827aa Fix reasoning/content separation and eliminate empty message divs
- Remove extractReasoningMiddleware as it expects <think> tags but API sends reasoning fields directly
- Ensure reasoning and content tokens create separate assistant messages
- Filter empty assistant messages at conversation level to prevent empty divs
- Add deduplication for tool results to prevent duplicates
- Only render content div when message has actual content
2025-06-25 11:51:41 +10:00
Glen Maddern
dffc1aa5fb Make thinking block collapse immediately when tool call starts
- End reasoning phase as soon as first tool-call event is detected
- Set reasoningEndTime and stop reasoning streaming immediately
- Creates linear flow: thinking expands → tool call (thinking collapses) → tool result → final response
- More aggressive/responsive UX that doesn't wait for final response to collapse thinking
2025-06-25 08:28:43 +10:00
Glen Maddern
8159950ffd Fix message ordering - create final response at end instead of updating existing
- Remove logic that updates existing assistant message
- Always create new final response message at end of conversation
- This ensures proper order: thinking → tool call → tool result → final response
- Still prevents duplicates by checking existing content
2025-06-25 08:25:03 +10:00
Glen Maddern
55d54bce60 Fix duplicate assistant messages by updating existing message
- Check if there's already an assistant message with only reasoning content
- Update existing assistant message with final content instead of creating new one
- Add duplicate detection to prevent multiple messages with same content
- Ensures only one assistant response appears after tool calls
2025-06-25 08:23:29 +10:00
Glen Maddern
0ed5dbaa67 Add intelligent content filtering to extract clean final answer
- Extract only the final answer from post-tool reasoning content
- Filter out thinking/reasoning patterns to prevent duplication
- Use regex matching and line filtering to identify clean response content
- Skip content that appears to be thinking rather than final answer
- Limit response length to prevent long thinking content from appearing
2025-06-25 08:21:08 +10:00
Glen Maddern
59026addde Fix message ordering and prevent content duplication in thinking/final response
- Collect post-tool reasoning content separately instead of processing immediately
- Process collected content as final assistant response after stream completes
- This ensures proper message order: thinking → tool call → tool result → final response
- Prevents duplication of thinking content in final response
2025-06-25 08:14:51 +10:00
Glen Maddern
d2b6f8013e Fix duplicate tool results and post-tool reasoning being hidden
- Add deduplication logic for tool results to prevent duplicates
- Detect when tool results have been processed and convert subsequent reasoning events to text content
- This fixes the issue where the final assistant response was hidden in the thinking block
- Tool results should now appear once and final response should appear as separate text
2025-06-24 22:49:20 +10:00
Glen Maddern
6126405ab0 Fix tool results being mixed into thinking blocks
- Add missing tool-result event handling in useStreamResponse
- Tool results now appear as separate ToolResultMessage components
- Prevents tool response content from being incorrectly added to reasoning content
- Maintains proper message order: thinking → tool call → tool result → assistant response
2025-06-24 22:43:06 +10:00
Glen Maddern
e7dd5776c3 Replace httpOnly with transportType option and add UI controls
- Replace httpOnly boolean with transportType ('auto'/'http'/'sse') option
- Add transport type cycling button next to 'Add New Server'
- Add blue transport badge next to connected server status
- Save transport preference to localStorage
- Update connection orchestration to handle sse-only and http-only modes
- Remove explicit fallback messaging since orchestration now controls it
2025-06-24 22:38:35 +10:00
Glen Maddern
03d95e70b7 Add httpOnly option to disable SSE fallback for debugging
- Added httpOnly option to UseMcpOptions to skip SSE fallback
- Updated all HTTP transport failure paths to respect httpOnly flag
- Temporarily enabled httpOnly in chat UI for streamable HTTP debugging
- Updated AGENT.md to require frequent commits after changes
2025-06-24 22:28:37 +10:00
Glen Maddern
ba9e6c9f3f Add debug logging and Accept headers for MCP transport debugging
- Added extensive debug logging to transport creation and connection flows
- Added proper Accept header (application/json, text/event-stream) to transport options
- Fixed scroll jittering during streaming with instant scroll behavior
- Investigating HTTP transport 'Not connected' errors and SSE connection hanging
2025-06-24 22:25:57 +10:00
Glen Maddern
ccad616e02 feat: Add sliding window effect for streaming reasoning text
- Use overflow:hidden + whitespace:nowrap to prevent line breaks
- Apply justify-end flexbox to push content to the right
- New tokens push older ones out to the left edge and hide them
- Creates smooth sliding window effect showing only recent tokens
- Only applies during streaming, keeps normal wrap after completion

This creates a nice visual effect where you can see the latest reasoning
tokens flowing in from right to left as the model thinks.
2025-06-24 20:18:40 +10:00
Glen Maddern
78e01032ae fix: Remove delay for instant reasoning block collapse
- Remove 500ms setTimeout delay when reasoning finishes
- Reasoning block now collapses immediately when response starts
- Creates smoother transition from thinking to response phase
- Eliminates jarring delay between reasoning end and collapse

The reasoning block now collapses instantly as soon as the model
starts generating the actual response, providing better UX flow.
2025-06-24 19:08:36 +10:00
Glen Maddern
b810f639aa fix: Create assistant message immediately when reasoning starts
- Create assistant message as soon as first reasoning event arrives
- This ensures reasoning block is visible from the beginning of thinking
- Prevent duplicate message creation in text-delta handler
- Move reasoning end time logic to text-delta start (when thinking finishes)

This fixes the issue where reasoning blocks weren't appearing because
the assistant message was only created after reasoning completed.
2025-06-24 19:03:43 +10:00
Glen Maddern
bfe2bd5816 feat: Implement live reasoning streaming with timing display
- Update ReasoningBlock to start expanded and show live reasoning content
- Add timing tracking with start/end timestamps for reasoning duration
- Auto-collapse reasoning blocks after completion with 500ms delay
- Show timing summary (e.g. 'Thought for 0.4s') instead of content when collapsed
- Add streaming cursor animation and real-time content updates
- Update types to support reasoning timing and streaming state
- Stream reasoning content live as tokens arrive during thinking phase

This provides a much better UX where users can see the model thinking
in real-time, then get a clean timing summary after completion.
2025-06-24 18:21:22 +10:00
Glen Maddern
68ecdebbc0 fix: Clean think tags from reasoning content before display
- Add cleaning logic to remove <think> and </think> tags from reasoning
- Trim whitespace after tag removal for clean display
- Add debug logging to show before/after cleaning
- The middleware extracts reasoning but includes the original tags

This ensures the reasoning blocks show clean thinking content without
the XML tags that were being captured along with the reasoning text.
2025-06-24 18:15:57 +10:00
Glen Maddern
33c914e19b debug: Add more detailed logging for reasoning event handling
- Add debug logs inside reasoning event handler to see if it's being triggered
- Log reasoning content length as it's being collected
- Add final reasoning content summary at end of stream
- Log why reasoning isn't being added if conditions aren't met
- Cast event to any to avoid potential TypeScript issues

This will help identify why reasoning events aren't being captured
despite appearing in the event stream logs.
2025-06-24 18:11:24 +10:00
Glen Maddern
ab9f3c0220 fix: Capture reasoning events during streaming instead of from result
- Add back reasoning variable to track content during streaming
- Handle reasoning events from extractReasoningMiddleware in stream processing
- Build up reasoning content from individual reasoning textDelta events
- Remove dependency on result.reasoning which wasn't being populated
- Add reasoning to assistant message after stream completes

The logs showed reasoning events were coming through correctly but we
weren't capturing them. This fixes the issue by collecting reasoning
content during streaming as the events arrive.
2025-06-24 17:53:12 +10:00
Glen Maddern
6c7422f832 fix: Add detailed debugging and use result.text for cleaned content
- Add debug logging to track aiResponse vs result.text content
- Use result.text from extractReasoningMiddleware for final content
- This should provide the properly cleaned text without thinking tags
- Comment out unused sidebar and edit functionality to fix TS errors

The result.text should contain content that has been automatically
cleaned by the extractReasoningMiddleware, while result.reasoning
contains the extracted thinking content.
2025-06-24 17:40:53 +10:00
Glen Maddern
03dd988f35 fix: Improve thinking tag removal with more aggressive cleaning
- Add multiple regex passes to handle various think tag scenarios
- Remove standalone <think> and </think> tags that might remain
- Clean leading whitespace after tag removal
- Add debug logging to track text cleaning process
- Handle streaming cases where tags appear before closing tags arrive

This provides more robust cleaning to ensure no think tags appear
in the final message content during streaming scenarios.
2025-06-24 17:35:14 +10:00
Glen Maddern
9a825d5afc fix: Remove thinking XML tags from main message content
- Add regex-based cleaning to remove <think>...</think> tags from display text
- Handle both closed and unclosed think tags in streaming content
- Apply cleaning only for reasoning-capable models to avoid unnecessary processing
- Update conversation content with cleaned response instead of raw response

This ensures thinking tags are extracted for reasoning display but don't
appear in the main message content, providing a clean user experience.
2025-06-24 17:28:08 +10:00
Glen Maddern
f53bc6867b Added a simpe Hono server to test 2025-06-24 17:26:16 +10:00
Glen Maddern
25857bc354 fix: Use extractReasoningMiddleware to properly parse thinking blocks
- Import extractReasoningMiddleware and wrapLanguageModel from AI SDK
- Wrap reasoning-capable models with extractReasoningMiddleware
- Use 'think' as tagName to parse <think> XML blocks from models
- Enable startWithReasoning option to handle models missing reasoning tags
- Extract reasoning from result.reasoning after stream completes
- Remove manual reasoning event handling and provider-specific config

This fixes the issue where thinking blocks appeared as raw XML tags
instead of parsed reasoning content. The middleware automatically
extracts reasoning from <think> tags and exposes it via result.reasoning.
2025-06-24 16:47:04 +10:00
Glen Maddern
62000dcdac feat: Add reasoning token support with expandable thinking blocks
- Add reasoning field to AssistantMessage type
- Create ReasoningBlock component with truncate/expand functionality
- Add reasoning-capable models (Qwen QwQ, DeepSeek R1, Qwen 3 32B)
- Implement reasoning event handling in stream processing
- Enable reasoning format for Groq and Anthropic models
- Style reasoning blocks with grey border and reduced font size
- Display reasoning above assistant messages when available

The reasoning blocks show truncated thinking tokens by default and
expand on click to show full reasoning process from models like
Qwen3-32b that support step-by-step reasoning output.
2025-06-24 16:41:56 +10:00
Glen Maddern
ace85a6c69 Added a simple example server for the demo 2025-06-24 16:19:32 +10:00
Glen Maddern
43dc39df96 BG css tweaks 2025-06-24 15:58:42 +10:00
Glen Maddern
8049c6bebe I am dum. CSS is smrt 2025-06-24 14:04:27 +10:00
Glen Maddern
5087ebacdc Update AGENT.md with chat UI development guidelines
- Add React Router and use-mcp to tech stack
- Document background animation best practices (hue-rotate, pseudo-elements)
- Modal patterns for UX (click-outside, cursor-pointer, backdrop styling)
- MCP server management (multiple servers, localStorage patterns, OAuth)
- State persistence guidelines (localStorage vs sessionStorage)
- UI indicator patterns (emoji-based, count formatting)
- Component organization for stateful elements
- Routing and OAuth callback implementation
- Performance considerations for animations and aggregation
2025-06-24 08:21:55 +10:00
Glen Maddern
a3200e09cb Doing some manual CSS hax for a change 2025-06-23 21:54:26 +10:00
Glen Maddern
71b07c488b Make gradient background repeat every 100vh
- Add background-size: 100% 100vh to fix gradient height to viewport
- Add background-repeat: repeat-y to tile gradient vertically
- Gradient pattern now repeats down the page as content grows
- Animation applies consistently to all repeated background tiles
- Creates seamless repeating pattern while maintaining hue rotation
2025-06-23 21:29:22 +10:00
Glen Maddern
4680b9d3e8 Make gradient background fill entire content height
- Change from h-dvh to min-h-screen on main container
- Remove overflow-clip to allow content to expand
- Update inner containers to use min-h-screen for proper height
- Background gradient now extends with content rather than fixed to viewport
- Allows for scrolling when content exceeds viewport height
2025-06-23 21:26:36 +10:00
Glen Maddern
ec7f1ff6dd Add Qwen 3 32B model under Groq provider
- Add qwen-3-32b model entry using qwen/qwen3-32b model ID
- Uses same Groq provider as Llama 3.3, sharing API token
- Model appears in selection modal between Llama and Claude
- Clean name 'Qwen 3 32B' for user interface
2025-06-23 21:18:22 +10:00
Glen Maddern
089c1c9f9e Deploying to chat.use-mcp.dev 2025-06-23 20:26:01 +10:00
Glen Maddern
78f0e364a6 Implement OAuth callback support for MCP servers
- Add react-router-dom dependency for routing support
- Create OAuthCallback component with animated background and loading spinner
- Add routing to App.tsx with /oauth/callback route
- Match styling with main app using animated-bg-container
- Use onMcpAuthorization from use-mcp library for OAuth handling
- Library already handles multiple server differentiation via state parameter
- Each MCP server gets unique OAuth flow identified by state containing server URL
- Clean up unused imports and variables in McpServerModal
2025-06-23 20:19:10 +10:00
Glen Maddern
32a6bf5abd Replace gradient with uniform 80% black background
- Remove radial gradient to fix banding issues
- Use solid 80% black background (rgba(0,0,0,0.8))
- Apply to both ModelSelectionModal and McpServerModal
- Clean, consistent backdrop without visual artifacts
2025-06-23 20:11:37 +10:00
Glen Maddern
272fd5606a Soften modal background with radial gradient
- Replace solid black 50% opacity with radial gradient
- Center: 80% black (rgba(0,0,0,0.8))
- Corners: 70% black (rgba(0,0,0,0.7))
- Apply to both ModelSelectionModal and McpServerModal
- Creates softer, more visually appealing backdrop
- Remove bg-black bg-opacity-50 classes and use inline style
2025-06-23 20:10:18 +10:00
Glen Maddern
1e68fca937 Change plug indicator format to use forward slashes
- Change from '0 (1) servers, 0 (2) tools' to '0/1 servers, 0/2 tools'
- Use forward slash format showing enabled/total for both servers and tools
- Cleaner, more compact visual representation
- Examples:
  - No servers: '0/0 servers, 0/0 tools'
  - One disabled server with 2 tools: '0/1 servers, 0/2 tools'
  - One enabled server with 2 tools: '1/1 servers, 2/2 tools'
2025-06-23 19:26:57 +10:00
Glen Maddern
0ce394f53c Update plug indicator format with comma separation
- Change format from '0 (1 servers) 0 (2 tools)' to '0 (1) servers, 0 (2) tools'
- Move words 'servers' and 'tools' outside brackets
- Add comma separation between server and tool counts
- More concise and readable format
2025-06-23 19:26:06 +10:00
Glen Maddern
644b3b663d Update plug indicator to show enabled vs total counts
- Change indicator format to show enabled/total for both servers and tools
- Format: 'X (Y servers) A (B tools)' where:
  - X = enabled servers, Y = total servers
  - A = tools from enabled servers, B = total tools from all servers
- Track tool counts per server in localStorage (mcpServerToolCounts)
- Persist tool counts even when servers are disabled
- Examples:
  - No servers: '0 (0 servers) 0 (0 tools)'
  - One disabled server with 2 tools: '0 (1 servers) 0 (2 tools)'
  - One enabled server with 2 tools: '1 (1 servers) 2 (2 tools)'
- Clean up tool counts when servers are deleted
2025-06-23 19:17:17 +10:00
Glen Maddern
d15c88419c Implement multiple MCP servers support
- Completely rewrite McpServerModal to support multiple servers
- Add McpServer interface with id, url, enabled, and name fields
- Use localStorage 'mcpServers' array instead of single server URL
- Add server list UI with status badges, toggle, and delete buttons
- Add 'Add New Server' section at bottom with input and Add button
- Support enabling/disabling individual servers with Power/PowerOff icons
- Delete servers with Trash2 icon in top right of each server block
- Aggregate tools from all enabled servers for parent component
- Render multiple McpConnection components for enabled servers
- Update plug indicator to show correct server count
- Add debug information panel showing server and tool counts
- Handle server names using hostname from URL
- Each server gets unique connection state tracking by server ID
2025-06-23 16:52:14 +10:00
Glen Maddern
1f80d7040b Add cursor pointer and click-outside-to-dismiss to model selector modal
- Add cursor-pointer class to X button in ModelSelectionModal
- Add onClick handler to backdrop overlay to close modal
- Add stopPropagation to modal content to prevent backdrop clicks
- Consistent modal behavior between model selector and MCP server modals
2025-06-23 16:13:33 +10:00
Glen Maddern
1514c6470e Add cursor pointer to X button and click-outside-to-dismiss
- Add cursor-pointer class to modal X button
- Add onClick handler to backdrop overlay to close modal
- Add stopPropagation to modal content to prevent backdrop clicks
- Now modal can be dismissed by clicking outside or using X button
2025-06-23 16:12:52 +10:00
Glen Maddern
812fdf92e3 Fix MCP tools count update in plug icon indicator
- Add onMcpToolsUpdate prop to ConversationThread interface
- Pass setMcpTools function from ChatApp down to ConversationThread
- Connect McpServerModal onToolsUpdate to parent state update
- Now plug icon will correctly show '1 (N)' when N tools are available
- Fixes issue where tool count remained at 0 despite successful connection
2025-06-23 16:11:42 +10:00
Glen Maddern
c3624e6bbc Keep MCP modal in React tree to maintain connection
- Replace conditional rendering with CSS visibility (hidden/block)
- Remove early return null when modal is closed
- McpConnection component stays mounted to preserve MCP server connection
- Modal is always in DOM but visually hidden when isOpen is false
- Prevents MCP disconnection when modal is dismissed
2025-06-23 16:09:15 +10:00
Glen Maddern
b1f7709a05 Move number to left of plug emoji
- Change layout from '🔌 0' to '0 🔌'
- Number now appears before the plug icon for better visual balance
- Maintains same functionality and styling
2025-06-23 16:02:59 +10:00
Glen Maddern
11360f38fc Add MCP server indicator with plug emoji and modal
- Create McpServerModal component based on existing McpServers component
- Add plug emoji (🔌) indicator on right side, opposite to brain emoji
- Show '0' when no servers connected, '1 (N)' when connected with N tools
- Fullscreen modal with MCP server configuration UI
- Support for single MCP server connection (foundation for multiple servers)
- Integrate with existing MCP tools logic and useMcp hook
- Clean layout with brain emoji on left, plug emoji on right
2025-06-23 16:02:05 +10:00
Glen Maddern
3461d3c900 Replace red X with grey 'none' text and add pointer cursor
- Change unconfigured model indicator from red ✕ to grey 'none' text
- Add explicit cursor-pointer class to brain emoji button
- Maintains grey text color consistent with configured model names
- Cleaner, less alarming visual for unconfigured state
2025-06-23 15:54:04 +10:00
Glen Maddern
50e97bb613 Isolate hue-rotate animation to background layer only
- Create separate background layer using ::before pseudo-element
- Move hue-rotate animation to background layer with z-index: -1
- Remove filter from main container to prevent inheritance
- Use equivalent gradient colors in CSS (pink-100, purple-50, indigo-100)
- Foreground elements (brain emoji, text) now maintain original colors
- Background still animates smoothly without affecting content
2025-06-23 15:52:34 +10:00
Glen Maddern
6a79264315 Add brain emoji model selector with fullscreen modal
- Create ModelSelectionModal component with list-style interface
- Add brain emoji (🧠) indicator below chat input, left-aligned
- Show current model name in lowercase or red X if not configured
- Fullscreen modal displays all available models with configuration status
- Green checkmark for configured models, red alert for unconfigured
- Integrate with existing API key management and model selection logic
- Modal handles API key setup workflow when selecting unconfigured models
- Clean separation between compact indicator and full selection interface
2025-06-23 15:49:45 +10:00
Glen Maddern
b33bde6b0e Use CSS custom property for cleaner random animation delay
- Move animation-delay to CSS using custom property --random-delay
- JavaScript still required to generate random value (CSS has no random functions)
- Cleaner separation between styling (CSS) and logic (JavaScript)
- Maintains same functionality with better organization
2025-06-21 13:58:11 +10:00
Glen Maddern
433eac58f1 Add random starting point for hue-rotate animation
- Generate random negative animation-delay between 0 and -60 seconds
- Starts animation at random point in the 60-second cycle
- Each page load will show different initial colors
- Uses useState with initializer function to set delay once on mount
- CSS doesn't have random functions, so JavaScript handles the randomization
2025-06-20 21:05:39 +10:00
Glen Maddern
d47124b8b3 Slow down hue-rotate animation to 60 seconds
- Change animation duration from 8s to 60s for subtle color shifting
- Creates gentle, barely noticeable background color transitions
- Maintains visual interest without being distracting
2025-06-20 21:04:37 +10:00
Glen Maddern
f94a80cbd9 Switch to hue-rotate animation for background gradient
- Revert complex conical gradient approach
- Return to original linear gradient (pink-100 → purple-50 → indigo-100)
- Add hue-rotate filter animation cycling through 360 degrees
- 8s duration for fast testing
- Simpler implementation with continuous color shifting
- Less control over saturation but smoother performance
2025-06-20 21:03:52 +10:00
Glen Maddern
668ecaff91 Add animated rotating conical gradient background
- Create large 500% conical gradient with pastel color cycle
- Position gradient off-screen (-200% top/left) for smooth rotation
- Add 8s linear infinite rotation animation for fast testing
- Use ::before pseudo-element to avoid repainting main content
- Cycle through various pastel shades (pink, purple, blue, green, yellow, orange)
- Performance-optimized: only transforms, no gradient repainting
2025-06-20 20:46:24 +10:00
Glen Maddern
15bb63670a Simplify chat UI to focus on main input with pastel gradient background
- Hide sidebar (conversations, model picker, MCP servers) while preserving components
- Hide navbar/title bar while preserving component
- Remove title text and description above input field
- Add pastel gradient background (pink-purple-indigo)
- Expand conversation thread to full width
- Create minimal, focused interface with only white input field visible
2025-06-20 17:19:54 +10:00
Glen Maddern
55c1756f36 deployng to workers.dev for sharing 2025-06-20 17:00:39 +10:00
Glen Maddern
6ebb156fe6 working tool call ui! 2025-06-19 19:46:33 +10:00
Glen Maddern
769475cecc Working on tool call display 2025-06-19 19:34:28 +10:00
Glen Maddern
d25f6c19a3 Moving most of the debug logging behind localStorage.USE_MCP_DEBUG 2025-06-19 14:31:30 +10:00
Glen Maddern
e94958865a MCP tool calling woo! 2025-06-19 14:07:28 +10:00
Glen Maddern
f5f1943ac5 Copying chat example across 2025-06-19 11:51:02 +10:00
82 changed files with 633 additions and 18231 deletions

View file

@ -1,50 +0,0 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Install dependencies - root
run: pnpm install
- name: Install dependencies - inspector example
run: pnpm install
working-directory: examples/inspector
- name: Install dependencies - cf-agents example
run: pnpm install
working-directory: examples/servers/cf-agents
- name: Install dependencies - hono-mcp example
run: pnpm install
working-directory: examples/servers/hono-mcp
- name: Install dependencies - test
run: pnpm install && pnpm exec playwright install
working-directory: test
- name: Run tests
run: pnpm test:headless
working-directory: test

View file

@ -1,11 +1,12 @@
name: Publish Any Commit
on:
pull_request:
push:
branches:
- '**'
tags:
- '!**'
# Temporarily disabled - uncomment to re-enable
# pull_request:
# push:
# branches:
# - '**'
# tags:
# - '!**'
jobs:
build:

2
.gitignore vendored
View file

@ -5,4 +5,4 @@ dist
.aider*
**/.claude/settings.local.json
/public
public

View file

@ -1 +0,0 @@
./scripts/pre-commit

View file

@ -1,21 +1,10 @@
# use-mcp Project Guidelines
## Build, Lint, and Test Commands
- `pnpm dev`: Run development build with watch mode and start all examples/servers
- Chat UI: http://localhost:5002
- Inspector: http://localhost:5001
- Hono MCP Server: http://localhost:5101
- CF Agents MCP Server: http://localhost:5102
- `pnpm dev`: Run development build with watch mode
- `pnpm build`: Build the project
- `pnpm check`: Run prettier checks and TypeScript type checking
### Integration Tests (in /test directory)
- `cd test && pnpm test`: Run integration tests headlessly (default)
- `cd test && pnpm test:headed`: Run integration tests with visible browser
- `cd test && pnpm test:headless`: Run integration tests headlessly
- `cd test && pnpm test:watch`: Run integration tests in watch mode
- `cd test && pnpm test:ui`: Run integration tests with interactive UI
## Code Style Guidelines
### Imports

View file

@ -8,7 +8,7 @@
A lightweight React hook for connecting to [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol) servers. Simplifies authentication and tool calling for AI systems implementing the MCP standard.
Try it out: [Chat Demo](https://chat.use-mcp.dev) | [MCP Inspector](https://inspector.use-mcp.dev) | [Cloudflare Workers AI Playground](https://playground.ai.cloudflare.com/)
Try it out: [MCP Inspector](https://inspector.use-mcp.dev) | [Cloudflare Workers AI Playground](https://playground.ai.cloudflare.com/)
## Installation
@ -20,39 +20,11 @@ pnpm add use-mcp
yarn add use-mcp
```
## Development
To run the development environment with all examples and servers:
```bash
pnpm dev
```
This starts:
- **Inspector**: http://localhost:5001 - MCP server debugging tool
- **Chat UI**: http://localhost:5002 - Example chat interface
- **Hono MCP Server**: http://localhost:5101 - Example MCP server
- **CF Agents MCP Server**: http://localhost:5102 - Cloudflare Workers AI MCP server
### Testing
Integration tests are located in the `test/` directory and run headlessly by default:
```bash
cd test && pnpm test # Run tests headlessly (default)
cd test && pnpm test:headed # Run tests with visible browser
cd test && pnpm test:watch # Run tests in watch mode
cd test && pnpm test:ui # Run tests with interactive UI
```
## Features
- 🔄 Automatic connection management with reconnection and retries
- 🔐 OAuth authentication flow handling with popup and fallback support
- 📦 Simple React hook interface for MCP integration
- 🧰 Full support for MCP tools, resources, and prompts
- 📄 Access server resources and read their contents
- 💬 Use server-provided prompt templates
- 🧰 TypeScript types for editor assistance and type checking
- 📝 Comprehensive logging for debugging
- 🌐 Works with both HTTP and SSE (Server-Sent Events) transports
@ -64,14 +36,10 @@ import { useMcp } from 'use-mcp/react'
function MyAIComponent() {
const {
state, // Connection state: 'discovering' | 'pending_auth' | 'authenticating' | 'connecting' | 'loading' | 'ready' | 'failed'
state, // Connection state: 'discovering' | 'authenticating' | 'connecting' | 'loading' | 'ready' | 'failed'
tools, // Available tools from MCP server
resources, // Available resources from MCP server
prompts, // Available prompts from MCP server
error, // Error message if connection failed
callTool, // Function to call tools on the MCP server
readResource, // Function to read resource contents
getPrompt, // Function to get prompt messages
retry, // Retry connection manually
authenticate, // Manually trigger authentication
clearStorage, // Clear stored tokens and credentials
@ -115,32 +83,6 @@ function MyAIComponent() {
))}
</ul>
<button onClick={handleSearch}>Search</button>
{/* Example: Display and read resources */}
{resources.length > 0 && (
<div>
<h3>Resources: {resources.length}</h3>
<button onClick={async () => {
const content = await readResource(resources[0].uri)
console.log('Resource content:', content)
}}>
Read First Resource
</button>
</div>
)}
{/* Example: Use prompts */}
{prompts.length > 0 && (
<div>
<h3>Prompts: {prompts.length}</h3>
<button onClick={async () => {
const result = await getPrompt(prompts[0].name)
console.log('Prompt messages:', result.messages)
}}>
Get First Prompt
</button>
</div>
)}
</div>
)
}
@ -225,27 +167,17 @@ function useMcp(options: UseMcpOptions): UseMcpResult
| `debug` | `boolean` | Whether to enable verbose debug logging |
| `autoRetry` | `boolean \| number` | Auto retry connection if initial connection fails, with delay in ms |
| `autoReconnect` | `boolean \| number` | Auto reconnect if an established connection is lost, with delay in ms (default: 3000) |
| `transportType` | `'auto' \| 'http' \| 'sse'` | Transport type preference: 'auto' (HTTP with SSE fallback), 'http' (HTTP only), 'sse' (SSE only) (default: 'auto') |
| `preventAutoAuth` | `boolean` | Prevent automatic authentication popup on initial connection (default: false) |
| `onPopupWindow` | `(url: string, features: string, window: Window \| null) => void` | Callback invoked just after the authentication popup window is opened |
#### Return Value
| Property | Type | Description |
|----------|------|-------------|
| `state` | `string` | Current connection state: 'discovering', 'pending_auth', 'authenticating', 'connecting', 'loading', 'ready', 'failed' |
| `state` | `string` | Current connection state: 'discovering', 'authenticating', 'connecting', 'loading', 'ready', 'failed' |
| `tools` | `Tool[]` | Available tools from the MCP server |
| `resources` | `Resource[]` | Available resources from the MCP server |
| `resourceTemplates` | `ResourceTemplate[]` | Available resource templates from the MCP server |
| `prompts` | `Prompt[]` | Available prompts from the MCP server |
| `error` | `string \| undefined` | Error message if connection failed |
| `authUrl` | `string \| undefined` | Manual authentication URL if popup is blocked |
| `log` | `{ level: 'debug' \| 'info' \| 'warn' \| 'error'; message: string; timestamp: number }[]` | Array of log messages |
| `callTool` | `(name: string, args?: Record<string, unknown>) => Promise<any>` | Function to call a tool on the MCP server |
| `listResources` | `() => Promise<void>` | Refresh the list of available resources |
| `readResource` | `(uri: string) => Promise<{ contents: Array<...> }>` | Read the contents of a specific resource |
| `listPrompts` | `() => Promise<void>` | Refresh the list of available prompts |
| `getPrompt` | `(name: string, args?: Record<string, string>) => Promise<{ messages: Array<...> }>` | Get a specific prompt with optional arguments |
| `retry` | `() => void` | Manually attempt to reconnect |
| `disconnect` | `() => void` | Disconnect from the MCP server |
| `authenticate` | `() => void` | Manually trigger authentication |

View file

@ -1,3 +0,0 @@
# OpenRouter OAuth Configuration
# Note: OpenRouter OAuth doesn't require client ID, only callback URL configuration
VITE_OPENROUTER_REDIRECT_URI=http://localhost:5002/oauth/openrouter/callback

View file

@ -1,92 +0,0 @@
# Model Selector Guide
This guide explains how to use the new favorites-based model selector system.
## Overview
The chat UI now supports:
- **39 models** from 3 providers (Anthropic, Groq, OpenRouter)
- **Favorites system** - star your preferred models
- **Search functionality** - find models by name or provider
- **Tool filtering** - show only models that support tool calling
- **OAuth authentication** - seamless OpenRouter integration
## Features
### Model Selection
- Click the model selector to view all available models
- Models are loaded from the live models.dev API
- Each model shows provider logo, name, and capabilities
### Favorites System
- ⭐ Star models to add them to your favorites
- Starred models are saved to local storage
- Use the "Favorites" filter to show only starred models
### Search & Filtering
- 🔍 Search box to find models by name or provider
- 🔧 "Tools Only" filter (appears when MCP tools are available)
- ⭐ "Favorites" filter to show only starred models
### Authentication
- **API Keys**: Enter manually for Anthropic and Groq
- **OAuth**: One-click authentication for OpenRouter
- Authentication status shown with icons (✓ or ⚠️)
## Provider Support
### Anthropic
- **Models**: 9 models including Claude 4 Sonnet
- **Auth**: API key (requires manual entry)
- **Tools**: All models support tool calling
### Groq
- **Models**: 13 models including Llama and Qwen variants
- **Auth**: API key (requires manual entry)
- **Tools**: 11 models support tool calling
### OpenRouter
- **Models**: 17 models from various providers
- **Auth**: OAuth PKCE flow (one-click authentication)
- **Tools**: All models support tool calling
## Setting Up OpenRouter OAuth
1. Create an OpenRouter account at https://openrouter.ai
2. Go to your dashboard and create an OAuth app
3. Set the redirect URI to: `http://localhost:5002/oauth/openrouter/callback`
4. Copy your client ID to your `.env` file:
```
VITE_OPENROUTER_CLIENT_ID=your_client_id_here
```
## Usage Tips
1. **Star your favorites** - This makes model selection much faster
2. **Use search** - With 39 models, search helps find what you need
3. **Filter by tools** - When using MCP tools, enable "Tools Only" filter
4. **Try different providers** - Each has unique models with different strengths
## Model Data Updates
Model data is fetched from models.dev API and can be updated with:
```bash
pnpm update-models
```
This will refresh the model list with the latest information from the API.
## Local Storage
The following preferences are saved locally:
- **Favorites**: `aiChatTemplate_favorites_v1`
- **Tokens**: `aiChatTemplate_token_[provider]`
- **Selected Model**: `aiChatTemplate_selectedModel`
## Troubleshooting
- **OAuth popup blocked**: Allow popups for this site
- **Authentication failed**: Check your API keys or re-authenticate
- **Model not working**: Verify the model supports the features you're using
- **Tools not showing**: Enable "Tools Only" filter when MCP tools are configured

View file

@ -1,36 +1,30 @@
# AI Chat with MCP
# AI chat template
A React-based AI chat application demonstrating [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) integration with multiple AI providers.
An unofficial template for ⚛️ React ⨉ ⚡️ Vite ⨉ ⛅️ Cloudflare Workers AI.
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.
**Live demo**: [chat.use-mcp.dev](https://chat.use-mcp.dev)
## Features
- **MCP Integration**: Connect to MCP servers with OAuth authentication support
- **Multi-model Support**: Anthropic (Claude) and Groq (Llama) models with API key authentication
- **Local Storage**: Conversations stored in browser's IndexedDB
- **Static Deployment**: Builds to static assets for deployment anywhere
- **Modern Stack**: React 19, TypeScript, Tailwind CSS, Vite
Full-stack AI chat application using Workers for the APIs (using the Cloudflare [vite-plugin](https://www.npmjs.com/package/@cloudflare/vite-plugin)) and Vite for the React application (hosted using [Workers Static Assets](https://developers.cloudflare.com/workers/static-assets/)). Provides chat functionality with [Workers AI](https://developers.cloudflare.com/workers-ai/), stores conversations in the browser's [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API), and uses [ai-sdk](https://sdk.vercel.ai/docs/introduction), [tailwindcss](https://tailwindcss.com/) and [workers-ai-provider](https://github.com/cloudflare/workers-ai-provider).
## Get started
```sh
pnpm install
pnpm dev
```
Build and deploy:
Create the project using [create-cloudflare](https://www.npmjs.com/package/create-cloudflare):
```sh
pnpm build
pnpm run deploy # deploys to Cloudflare Pages
npm create cloudflare@latest -- --template thomasgauvin/ai-chat-template
```
## Development
Run the project and deploy it:
- **Dev server**: `pnpm dev` (runs on port 5002)
- **Build**: `pnpm build`
- **Lint**: `pnpm lint`
- **Test**: `pnpm test` (Playwright E2E tests)
```sh
cd <project-name>
npm install
npm run dev
```
```
npm run deploy
```
## What's next?
- Change the name of the package (in `package.json`)
- Change the name of the worker (in `wrangler.jsonc`)

View file

@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port=5002",
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
@ -12,13 +12,11 @@
"test": "playwright test",
"test:ui": "playwright test --ui",
"test:headed": "playwright test --headed",
"test:html": "playwright test --reporter=html",
"update-models": "tsx scripts/update-models.ts"
"test:html": "playwright test --reporter=html"
},
"dependencies": {
"@ai-sdk/anthropic": "^1.2.12",
"@ai-sdk/groq": "^1.2.9",
"@ai-sdk/openai": "^1.3.23",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.0.14",
"ai": "^4.1.61",
@ -32,6 +30,7 @@
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.1",
"tailwindcss": "^4.0.14",
"use-mcp": "link:../..",
"workers-ai-provider": "^0.1.3"
},
"devDependencies": {
@ -48,10 +47,8 @@
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"prettier": "^3.5.3",
"tsx": "^4.20.3",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"use-mcp": "link:../..",
"vite": "^6.2.0",
"wrangler": "^4.1.0"
},

View file

@ -14,15 +14,12 @@ importers:
'@ai-sdk/groq':
specifier: ^1.2.9
version: 1.2.9(zod@3.24.4)
'@ai-sdk/openai':
specifier: ^1.3.23
version: 1.3.23(zod@3.24.4)
'@tailwindcss/typography':
specifier: ^0.5.16
version: 0.5.16(tailwindcss@4.1.6)
'@tailwindcss/vite':
specifier: ^4.0.14
version: 4.1.6(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3))
version: 4.1.6(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))
ai:
specifier: ^4.1.61
version: 4.3.15(react@19.1.0)(zod@3.24.4)
@ -56,6 +53,9 @@ importers:
tailwindcss:
specifier: ^4.0.14
version: 4.1.6
use-mcp:
specifier: link:../..
version: link:../..
workers-ai-provider:
specifier: ^0.1.3
version: 0.1.3(react@19.1.0)
@ -65,7 +65,7 @@ importers:
version: 1.2.11(zod@3.24.4)
'@cloudflare/vite-plugin':
specifier: ^0.1.12
version: 0.1.21(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3))(workerd@1.20250507.0)(wrangler@4.14.4(@cloudflare/workers-types@4.20250510.0))
version: 0.1.21(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))(workerd@1.20250507.0)(wrangler@4.14.4(@cloudflare/workers-types@4.20250510.0))
'@cloudflare/workers-types':
specifier: ^4.20250317.0
version: 4.20250510.0
@ -83,7 +83,7 @@ importers:
version: 19.1.3(@types/react@19.1.3)
'@vitejs/plugin-react':
specifier: ^4.3.4
version: 4.4.1(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3))
version: 4.4.1(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))
eslint:
specifier: ^9.21.0
version: 9.26.0(jiti@2.4.2)
@ -99,21 +99,15 @@ importers:
prettier:
specifier: ^3.5.3
version: 3.5.3
tsx:
specifier: ^4.20.3
version: 4.20.3
typescript:
specifier: ~5.7.2
version: 5.7.3
typescript-eslint:
specifier: ^8.24.1
version: 8.32.0(eslint@9.26.0(jiti@2.4.2))(typescript@5.7.3)
use-mcp:
specifier: link:../..
version: link:../..
vite:
specifier: ^6.2.0
version: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3)
version: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2)
wrangler:
specifier: ^4.1.0
version: 4.14.4(@cloudflare/workers-types@4.20250510.0)
@ -132,12 +126,6 @@ packages:
peerDependencies:
zod: ^3.0.0
'@ai-sdk/openai@1.3.23':
resolution: {integrity: sha512-86U7rFp8yacUAOE/Jz8WbGcwMCqWvjK33wk5DXkfnAOEn3mx2r7tNSJdjukQFZbAK97VMXGPPHxF+aEARDXRXQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
'@ai-sdk/provider-utils@1.0.22':
resolution: {integrity: sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==}
engines: {node: '>=18'}
@ -1508,9 +1496,6 @@ packages:
get-source@2.0.12:
resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==}
get-tsconfig@4.10.1:
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@ -2201,9 +2186,6 @@ packages:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@ -2376,11 +2358,6 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsx@4.20.3:
resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==}
engines: {node: '>=18.0.0'}
hasBin: true
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@ -2595,12 +2572,6 @@ snapshots:
'@ai-sdk/provider-utils': 2.2.8(zod@3.24.4)
zod: 3.24.4
'@ai-sdk/openai@1.3.23(zod@3.24.4)':
dependencies:
'@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.8(zod@3.24.4)
zod: 3.24.4
'@ai-sdk/provider-utils@1.0.22(zod@3.24.4)':
dependencies:
'@ai-sdk/provider': 0.0.26
@ -2767,7 +2738,7 @@ snapshots:
optionalDependencies:
workerd: 1.20250507.0
'@cloudflare/vite-plugin@0.1.21(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3))(workerd@1.20250507.0)(wrangler@4.14.4(@cloudflare/workers-types@4.20250510.0))':
'@cloudflare/vite-plugin@0.1.21(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))(workerd@1.20250507.0)(wrangler@4.14.4(@cloudflare/workers-types@4.20250510.0))':
dependencies:
'@cloudflare/unenv-preset': 2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250507.0)
'@hattip/adapter-node': 0.0.49
@ -2776,7 +2747,7 @@ snapshots:
picocolors: 1.1.1
tinyglobby: 0.2.13
unenv: 2.0.0-rc.15
vite: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3)
vite: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2)
wrangler: 4.14.4(@cloudflare/workers-types@4.20250510.0)
ws: 8.18.0
transitivePeerDependencies:
@ -3251,12 +3222,12 @@ snapshots:
postcss-selector-parser: 6.0.10
tailwindcss: 4.1.6
'@tailwindcss/vite@4.1.6(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3))':
'@tailwindcss/vite@4.1.6(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))':
dependencies:
'@tailwindcss/node': 4.1.6
'@tailwindcss/oxide': 4.1.6
tailwindcss: 4.1.6
vite: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3)
vite: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2)
'@types/babel__core@7.20.5':
dependencies:
@ -3394,14 +3365,14 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
'@vitejs/plugin-react@4.4.1(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3))':
'@vitejs/plugin-react@4.4.1(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))':
dependencies:
'@babel/core': 7.27.1
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1)
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1)
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
vite: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3)
vite: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2)
transitivePeerDependencies:
- supports-color
@ -3913,10 +3884,6 @@ snapshots:
data-uri-to-buffer: 2.0.2
source-map: 0.6.1
get-tsconfig@4.10.1:
dependencies:
resolve-pkg-maps: 1.0.0
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@ -4791,8 +4758,6 @@ snapshots:
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
reusify@1.1.0: {}
rollup@4.40.2:
@ -5020,13 +4985,6 @@ snapshots:
tslib@2.8.1: {}
tsx@4.20.3:
dependencies:
esbuild: 0.25.4
get-tsconfig: 4.10.1
optionalDependencies:
fsevents: 2.3.3
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
@ -5133,7 +5091,7 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.2
vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3):
vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2):
dependencies:
esbuild: 0.25.4
fdir: 6.4.4(picomatch@4.0.2)
@ -5145,7 +5103,6 @@ snapshots:
fsevents: 2.3.3
jiti: 2.4.2
lightningcss: 1.29.2
tsx: 4.20.3
which@2.0.2:
dependencies:

View file

@ -1,105 +0,0 @@
#!/usr/bin/env tsx
import { writeFileSync } from 'fs'
import { join } from 'path'
interface ModelData {
id: string
name: string
attachment: boolean
reasoning: boolean
temperature: boolean
tool_call: boolean
knowledge: string
release_date: string
last_updated: string
modalities: {
input: string[]
output: string[]
}
open_weights: boolean
limit: {
context: number
output: number
}
cost?: {
input: number
output: number
cache_read?: number
cache_write?: number
}
}
interface ProviderData {
models: Record<string, ModelData>
}
interface ModelsDevData {
anthropic: ProviderData
groq: ProviderData
openrouter: ProviderData
[key: string]: ProviderData
}
const SUPPORTED_PROVIDERS = ['anthropic', 'groq', 'openrouter'] as const
type SupportedProvider = (typeof SUPPORTED_PROVIDERS)[number]
async function fetchModelsData(): Promise<ModelsDevData> {
console.log('Fetching models data from models.dev...')
const response = await fetch('https://models.dev/api.json')
if (!response.ok) {
throw new Error(`Failed to fetch models data: ${response.status} ${response.statusText}`)
}
return await response.json()
}
function filterAndTransformModels(data: ModelsDevData) {
const filtered: Record<SupportedProvider, Record<string, ModelData>> = {
anthropic: {},
groq: {},
openrouter: {},
}
// Filter by supported providers
for (const provider of SUPPORTED_PROVIDERS) {
if (data[provider] && data[provider].models) {
filtered[provider] = data[provider].models
}
}
return filtered
}
async function main() {
try {
const data = await fetchModelsData()
const filteredData = filterAndTransformModels(data)
const outputPath = join(process.cwd(), 'src', 'data', 'models.json')
writeFileSync(outputPath, JSON.stringify(filteredData, null, 2))
console.log(`✅ Models data updated successfully at ${outputPath}`)
// Print summary
let totalModels = 0
let toolSupportingModels = 0
for (const [provider, models] of Object.entries(filteredData)) {
const modelCount = Object.keys(models).length
const toolModels = Object.values(models).filter((m) => m.tool_call).length
console.log(` ${provider}: ${modelCount} models (${toolModels} support tools)`)
totalModels += modelCount
toolSupportingModels += toolModels
}
console.log(`\nTotal: ${totalModels} models (${toolSupportingModels} support tools)`)
} catch (error) {
console.error('❌ Failed to update models data:', error)
process.exit(1)
}
}
main()

View file

@ -1,14 +1,11 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import ChatApp from './components/ChatApp'
import PkceCallback from './components/PkceCallback.tsx'
import { OAuthCallback } from './components/OAuthCallback.tsx'
import { OAuthCallback } from './components/OAuthCallback'
function App() {
return (
<Router>
<Routes>
<Route path="/oauth/groq/callback" element={<PkceCallback provider="groq" />} />
<Route path="/oauth/openrouter/callback" element={<PkceCallback provider="openrouter" />} />
<Route path="/oauth/callback" element={<OAuthCallback />} />
<Route path="/" element={<ChatApp />} />
</Routes>

View file

@ -1,12 +1,12 @@
import React, { useState } from 'react'
import { createPortal } from 'react-dom'
import { X, Eye, EyeOff } from 'lucide-react'
import { type Provider } from '../types/models'
import { type ModelProvider } from '../types/models'
interface ApiKeyModalProps {
isOpen: boolean
onClose: () => void
provider: Provider
provider: ModelProvider
onSave: (apiKey: string) => void
}

View file

@ -1,13 +1,10 @@
import React, { useState, useEffect } from 'react'
import ConversationThread from './ConversationThread.tsx'
import ChatSidebar from './ChatSidebar'
import ChatNavbar from './ChatNavbar'
import { storeName } from '../consts.ts'
import { type Conversation } from '../types'
import { useIndexedDB } from '../hooks/useIndexedDB'
import { type Model } from '../types/models'
import { getSelectedModel, setSelectedModel as saveSelectedModel } from '../utils/modelPreferences'
import { useModels } from '../hooks/useModels'
import { type IDBPDatabase } from 'idb'
import { type Tool } from 'use-mcp/react'
@ -17,123 +14,17 @@ interface ChatAppProps {}
const ChatApp: React.FC<ChatAppProps> = () => {
const [conversations, setConversations] = useState<Conversation[]>([])
const [conversationId, setConversationId] = useState<number | undefined>(undefined)
const [sidebarVisible, setSidebarVisible] = useState(false)
const [selectedModel, setSelectedModel] = useState<Model>(getSelectedModel())
const [apiKeyUpdateTrigger, setApiKeyUpdateTrigger] = useState<number>(0)
const [mcpTools, setMcpTools] = useState<Tool[]>([])
const [animationDelay] = useState<number>(() => -Math.random() * 60)
const db = useIndexedDB()
const { models, addToFavorites, toggleFavorite, isFavorite, getFavoriteModels } = useModels()
const handleApiKeyUpdate = () => {
setApiKeyUpdateTrigger((prev) => prev + 1)
}
// Handle OAuth success messages from popups
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
console.log('DEBUG: Received message in parent window:', event.data)
if (event.data.type === 'oauth_success') {
console.log('DEBUG: OAuth success message received, triggering API key update')
handleApiKeyUpdate()
}
}
window.addEventListener('message', handleMessage)
return () => window.removeEventListener('message', handleMessage)
}, [])
// Poll for OAuth token changes (fallback for when popup messaging doesn't work)
useEffect(() => {
const oauthProviders = ['groq', 'openrouter'] as const
let initialTokens: Record<string, string | null> = {}
// Capture initial token state
const captureInitialTokens = () => {
for (const providerId of oauthProviders) {
const tokenKey = `aiChatTemplate_token_${providerId}`
initialTokens[providerId] = localStorage.getItem(tokenKey)
}
}
const checkForNewTokens = () => {
for (const providerId of oauthProviders) {
const tokenKey = `aiChatTemplate_token_${providerId}`
const currentToken = localStorage.getItem(tokenKey)
// Check if token was added or changed
if (currentToken !== initialTokens[providerId]) {
try {
const parsedToken = JSON.parse(currentToken || '{}')
if (parsedToken.access_token) {
console.log('DEBUG: New OAuth token detected for', providerId, 'via polling')
handleApiKeyUpdate()
return true // Stop polling once we find a new token
}
} catch (e) {
// Invalid token format, continue checking
}
}
}
return false
}
let pollInterval: number | null = null
const startPolling = () => {
// Capture initial state
captureInitialTokens()
// Check immediately for existing valid tokens (in case we just redirected from OAuth)
console.log('DEBUG: Checking for existing OAuth tokens on startup')
for (const providerId of oauthProviders) {
const tokenKey = `aiChatTemplate_token_${providerId}`
const currentToken = localStorage.getItem(tokenKey)
console.log(`DEBUG: Checking ${providerId} token:`, currentToken ? 'exists' : 'not found')
if (currentToken) {
try {
const parsedToken = JSON.parse(currentToken)
console.log(`DEBUG: Parsed ${providerId} token:`, parsedToken)
if (parsedToken.access_token) {
console.log('DEBUG: Found existing OAuth token for', providerId, 'on startup')
handleApiKeyUpdate()
return // Don't start polling if we found a valid token
}
} catch (e) {
console.log(`DEBUG: Failed to parse ${providerId} token:`, e)
}
}
}
// Start polling every 500ms for new tokens
pollInterval = setInterval(() => {
if (checkForNewTokens()) {
if (pollInterval) {
clearInterval(pollInterval)
pollInterval = null
console.log('DEBUG: Stopped polling for OAuth tokens')
}
}
}, 500)
console.log('DEBUG: Started polling for OAuth token changes')
}
// Start polling after a short delay to allow for popup messages first
const delayedStart = setTimeout(startPolling, 100)
return () => {
if (delayedStart) clearTimeout(delayedStart)
if (pollInterval) {
clearInterval(pollInterval)
console.log('DEBUG: Cleaned up OAuth token polling')
}
}
}, [])
const handleModelChange = (model: Model) => {
setSelectedModel(model)
saveSelectedModel(model)
@ -174,12 +65,14 @@ const ChatApp: React.FC<ChatAppProps> = () => {
}
}
const editConversationTitle = async (id: number, newTitle: string) => {
const conversation = (await db!.get(storeName, id)) as Conversation
conversation.title = newTitle
await db!.put(storeName, conversation)
setConversations((prev) => prev.map((conv) => (conv.id === id ? { ...conv, title: newTitle } : conv)))
}
// const editConversationTitle = async (id: number, newTitle: string) => {
// const conversation = (await db!.get(storeName, id)) as Conversation;
// conversation.title = newTitle;
// await db!.put(storeName, conversation);
// setConversations((prev) =>
// prev.map((conv) => (conv.id === id ? { ...conv, title: newTitle } : conv))
// );
// };
const startNewConversation = async () => {
//create unique id for new conversation
@ -206,27 +99,29 @@ const ChatApp: React.FC<ChatAppProps> = () => {
style={{ '--random-delay': `${animationDelay}s` } as React.CSSProperties}
>
<div className="flex flex-row flex-grow flex-1 min-h-screen relative">
{/* Sidebar and Navbar components */}
{false && (
<>
<ChatSidebar
sidebarVisible={sidebarVisible}
setSidebarVisible={setSidebarVisible}
conversations={conversations}
conversationId={conversationId}
setConversationId={setConversationId}
deleteConversation={deleteConversation}
editConversationTitle={editConversationTitle}
startNewConversation={startNewConversation}
selectedModel={selectedModel}
onModelChange={handleModelChange}
apiKeyUpdateTrigger={apiKeyUpdateTrigger}
onMcpToolsUpdate={setMcpTools}
mcpTools={mcpTools}
/>
<ChatNavbar sidebarVisible={sidebarVisible} setSidebarVisible={setSidebarVisible} />
</>
)}
{/* Sidebar and Navbar components hidden but kept in codebase */}
{/*{false && (*/}
{/* <>*/}
{/* <ChatSidebar*/}
{/* sidebarVisible={sidebarVisible}*/}
{/* setSidebarVisible={setSidebarVisible}*/}
{/* conversations={conversations}*/}
{/* conversationId={conversationId}*/}
{/* setConversationId={setConversationId}*/}
{/* deleteConversation={deleteConversation}*/}
{/* editConversationTitle={editConversationTitle}*/}
{/* startNewConversation={startNewConversation}*/}
{/* selectedModel={selectedModel}*/}
{/* onModelChange={handleModelChange}*/}
{/* apiKeyUpdateTrigger={apiKeyUpdateTrigger}*/}
{/* onMcpToolsUpdate={setMcpTools}*/}
{/* />*/}
{/* <ChatNavbar*/}
{/* sidebarVisible={sidebarVisible}*/}
{/* setSidebarVisible={setSidebarVisible}*/}
{/* />*/}
{/* </>*/}
{/*)}*/}
<div className="flex flex-col flex-grow h-full w-full">
<ConversationThread
conversations={conversations}
@ -240,11 +135,6 @@ const ChatApp: React.FC<ChatAppProps> = () => {
apiKeyUpdateTrigger={apiKeyUpdateTrigger}
mcpTools={mcpTools}
onMcpToolsUpdate={setMcpTools}
addToFavorites={addToFavorites}
models={models}
toggleFavorite={toggleFavorite}
isFavorite={isFavorite}
getFavoriteModels={getFavoriteModels}
/>
</div>
</div>

View file

@ -25,7 +25,6 @@ interface ChatSidebarProps {
onModelChange: (model: Model) => void
apiKeyUpdateTrigger: number
onMcpToolsUpdate: (tools: Tool[]) => void
mcpTools: Tool[]
}
const ChatSidebar: React.FC<ChatSidebarProps> = ({
@ -41,7 +40,6 @@ const ChatSidebar: React.FC<ChatSidebarProps> = ({
onModelChange,
apiKeyUpdateTrigger,
onMcpToolsUpdate,
mcpTools,
}) => {
const handleConversationClick = (id: number | undefined) => {
setConversationId(id)
@ -137,12 +135,7 @@ const ChatSidebar: React.FC<ChatSidebarProps> = ({
{/* Model Selector and MCP Servers at bottom */}
<div className="p-2 border-t border-zinc-200 space-y-3">
<ModelSelector
selectedModel={selectedModel}
onModelChange={onModelChange}
apiKeyUpdateTrigger={apiKeyUpdateTrigger}
toolsAvailable={mcpTools.length > 0}
/>
<ModelSelector selectedModel={selectedModel} onModelChange={onModelChange} apiKeyUpdateTrigger={apiKeyUpdateTrigger} />
<McpServers onToolsUpdate={onMcpToolsUpdate} />
</div>
</div>

View file

@ -2,7 +2,6 @@ import React, { useState, useEffect, FormEvent } from 'react'
import { storeName } from '../consts'
import '../styles/scrollbar.css'
import '../styles/github.css'
import '../styles/markdown.css'
import { type Conversation, type Message } from '../types'
import { type Model } from '../types/models'
import { type IDBPDatabase } from 'idb'
@ -30,11 +29,6 @@ interface ConversationThreadProps {
apiKeyUpdateTrigger: number
mcpTools: Tool[]
onMcpToolsUpdate: (tools: Tool[]) => void
addToFavorites: (modelId: string) => void
models: Model[]
toggleFavorite: (modelId: string) => void
isFavorite: (modelId: string) => boolean
getFavoriteModels: () => Model[]
}
const ConversationThread: React.FC<ConversationThreadProps> = ({
@ -49,11 +43,6 @@ const ConversationThread: React.FC<ConversationThreadProps> = ({
apiKeyUpdateTrigger,
mcpTools,
onMcpToolsUpdate,
addToFavorites,
models,
toggleFavorite,
isFavorite,
getFavoriteModels,
}) => {
const [input, setInput] = useState<string>('')
const [apiKeyModal, setApiKeyModal] = useState<{ isOpen: boolean; model: Model | null }>({
@ -268,17 +257,7 @@ const ConversationThread: React.FC<ConversationThreadProps> = ({
<ApiKeyModal
isOpen={apiKeyModal.isOpen}
onClose={handleApiKeyCancel}
provider={
apiKeyModal.model?.provider ?? {
id: 'unknown' as any,
name: 'Unknown',
baseUrl: '',
logo: '',
documentationUrl: '',
authType: 'apiKey',
apiKeyHeader: '',
}
}
provider={apiKeyModal.model?.provider ?? { id: '', name: '', baseUrl: '', apiKeyHeader: '', documentationUrl: '' }}
onSave={handleApiKeySave}
/>
@ -288,11 +267,6 @@ const ConversationThread: React.FC<ConversationThreadProps> = ({
selectedModel={selectedModel}
onModelChange={onModelChange}
apiKeyUpdateTrigger={apiKeyUpdateTrigger}
addToFavorites={addToFavorites}
models={models}
toggleFavorite={toggleFavorite}
isFavorite={isFavorite}
getFavoriteModels={getFavoriteModels}
/>
<McpServerModal isOpen={mcpServerModal} onClose={() => setMcpServerModal(false)} onToolsUpdate={onMcpToolsUpdate} />

View file

@ -11,7 +11,13 @@ interface McpServer {
}
// MCP Connection wrapper for a single server
function McpConnection({ server, onConnectionUpdate }: { server: McpServer; onConnectionUpdate: (serverId: string, data: any) => void }) {
function McpConnection({
server,
onConnectionUpdate,
}: {
server: McpServer
onConnectionUpdate: (serverId: string, data: any) => void
}) {
// Use the MCP hook with the server URL
const connection = useMcp({
url: server.url,
@ -19,7 +25,6 @@ function McpConnection({ server, onConnectionUpdate }: { server: McpServer; onCo
autoRetry: false,
popupFeatures: 'width=500,height=600,resizable=yes,scrollbars=yes',
transportType: server.transportType,
preventAutoAuth: true, // Prevent automatic popups on page load
})
// Update parent component with connection data
@ -49,12 +54,28 @@ const McpServerModal: React.FC<McpServerModalProps> = ({ isOpen, onClose, onTool
const stored = localStorage.getItem('mcpServerToolCounts')
return stored ? JSON.parse(stored) : {}
})
const [transportType, _setTransportType] = useState<'auto' | 'http' | 'sse'>(() => {
const [transportType, setTransportType] = useState<'auto' | 'http' | 'sse'>(() => {
const stored = localStorage.getItem('mcpTransportType')
return (stored as 'auto' | 'http' | 'sse') || 'http'
})
const [newServerTransportType, setNewServerTransportType] = useState<'auto' | 'http' | 'sse'>('http')
// Helper to cycle through transport types
const cycleTransportType = () => {
setTransportType((current) => {
switch (current) {
case 'auto':
return 'http'
case 'http':
return 'sse'
case 'sse':
return 'auto'
default:
return 'auto'
}
})
}
// Helper to cycle through new server transport types
const cycleNewServerTransportType = () => {
setNewServerTransportType((current) => {
@ -201,8 +222,6 @@ const McpServerModal: React.FC<McpServerModalProps> = ({ isOpen, onClose, onTool
switch (state) {
case 'discovering':
return <span className={`${baseClasses} bg-blue-100 text-blue-800`}>Discovering</span>
case 'pending_auth':
return <span className={`${baseClasses} bg-orange-100 text-orange-800`}>Authentication Required</span>
case 'authenticating':
return <span className={`${baseClasses} bg-purple-100 text-purple-800`}>Authenticating</span>
case 'connecting':
@ -296,31 +315,18 @@ const McpServerModal: React.FC<McpServerModalProps> = ({ isOpen, onClose, onTool
<div className="text-sm text-red-600 p-3 bg-red-50 rounded border mb-3">{error}</div>
)}
{(state === 'pending_auth' || authUrl) && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded mb-3">
<p className="text-sm mb-2">
{state === 'pending_auth'
? 'Authentication is required to connect to this server.'
: 'Authentication popup was blocked. You can open the authentication page manually:'}
</p>
<div className="space-y-2">
<button
className="w-full px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm font-medium"
onClick={() => handleManualAuth(server.id)}
>
Open Authentication Popup
</button>
{authUrl && (
<a
href={authUrl}
target="_blank"
rel="noopener noreferrer"
className="block text-center text-sm text-blue-700 hover:text-blue-800 underline"
>
Or open in new tab instead
</a>
)}
</div>
{authUrl && (
<div className="p-3 bg-orange-50 border border-orange-200 rounded mb-3">
<p className="text-sm mb-2">Authentication required. Please click the link below:</p>
<a
href={authUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-orange-700 hover:text-orange-800 underline"
onClick={() => handleManualAuth(server.id)}
>
Authenticate in new window
</a>
</div>
)}

View file

@ -10,7 +10,6 @@ function McpConnection({ serverUrl, onConnectionUpdate }: { serverUrl: string; o
debug: true,
autoRetry: false,
popupFeatures: 'width=500,height=600,resizable=yes,scrollbars=yes',
preventAutoAuth: true, // Prevent automatic popups on page load
})
// Update parent component with connection data
@ -104,8 +103,6 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
switch (state) {
case 'discovering':
return <span className={`${baseClasses} bg-blue-100 text-blue-800`}>Discovering</span>
case 'pending_auth':
return <span className={`${baseClasses} bg-orange-100 text-orange-800`}>Authentication Required</span>
case 'authenticating':
return <span className={`${baseClasses} bg-purple-100 text-purple-800`}>Authenticating</span>
case 'connecting':
@ -187,32 +184,19 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
)}
</div>
{/* Authentication Action for pending_auth or existing authUrl */}
{(state === 'pending_auth' || authUrl) && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded">
<p className="text-xs mb-2">
{state === 'pending_auth'
? 'Authentication is required to connect to this server.'
: 'Authentication popup was blocked. You can open the authentication page manually:'}
</p>
<div className="space-y-2">
<button
className="w-full px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs font-medium"
onClick={handleManualAuth}
>
Open Authentication Popup
</button>
{authUrl && (
<a
href={authUrl}
target="_blank"
rel="noopener noreferrer"
className="block text-center text-xs text-blue-700 hover:text-blue-800 underline"
>
Or open in new tab instead
</a>
)}
</div>
{/* Authentication Link if needed */}
{authUrl && (
<div className="p-3 bg-orange-50 border border-orange-200 rounded">
<p className="text-xs mb-2">Authentication required. Please click the link below:</p>
<a
href={authUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-orange-700 hover:text-orange-800 underline"
onClick={handleManualAuth}
>
Authenticate in new window
</a>
</div>
)}

View file

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'
import { X, AlertCircle, CheckCircle, Star, ArrowLeft, Search } from 'lucide-react'
import { type Model, type Provider, providers } from '../types/models'
import { hasApiKey, setApiKey, clearApiKey, beginOAuthFlow } from '../utils/auth'
import { X, AlertCircle, CheckCircle } from 'lucide-react'
import { type Model, availableModels } from '../types/models'
import { hasApiKey, setApiKey, clearApiKey } from '../utils/apiKeys'
import ApiKeyModal from './ApiKeyModal'
interface ModelSelectionModalProps {
@ -10,11 +10,6 @@ interface ModelSelectionModalProps {
selectedModel: Model
onModelChange: (model: Model) => void
apiKeyUpdateTrigger: number
addToFavorites: (modelId: string) => void
models: Model[]
toggleFavorite: (modelId: string) => void
isFavorite: (modelId: string) => boolean
getFavoriteModels: () => Model[]
}
const ModelSelectionModal: React.FC<ModelSelectionModalProps> = ({
@ -23,28 +18,20 @@ const ModelSelectionModal: React.FC<ModelSelectionModalProps> = ({
selectedModel,
onModelChange,
apiKeyUpdateTrigger,
addToFavorites,
models,
toggleFavorite,
isFavorite,
getFavoriteModels,
}) => {
const [apiKeyModal, setApiKeyModal] = useState<{ isOpen: boolean; model: Model | null }>({
isOpen: false,
model: null,
})
const [currentView, setCurrentView] = useState<'main' | 'provider'>('main')
const [selectedProvider, setSelectedProvider] = useState<Provider | null>(null)
const [searchTerm, setSearchTerm] = useState('')
const [apiKeyStatuses, setApiKeyStatuses] = useState<Record<string, boolean>>({})
useEffect(() => {
const statuses: Record<string, boolean> = {}
models.forEach((model) => {
availableModels.forEach((model) => {
statuses[model.provider.id] = hasApiKey(model.provider.id)
})
setApiKeyStatuses(statuses)
}, [apiKeyUpdateTrigger, models])
}, [apiKeyUpdateTrigger])
useEffect(() => {
if (isOpen) {
@ -58,104 +45,33 @@ const ModelSelectionModal: React.FC<ModelSelectionModalProps> = ({
}
}, [isOpen])
// Handle OAuth success - show provider models when OAuth completes
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.data.type === 'oauth_success' && event.data.provider && isOpen) {
const provider = providers[event.data.provider as keyof typeof providers]
if (provider) {
setCurrentView('provider')
setSelectedProvider(provider)
}
}
}
const handleModelSelect = (model: Model) => {
const hasKey = hasApiKey(model.provider.id)
window.addEventListener('message', handleMessage)
return () => window.removeEventListener('message', handleMessage)
}, [isOpen])
// Reset view when modal closes
useEffect(() => {
if (!isOpen) {
setCurrentView('main')
setSelectedProvider(null)
setSearchTerm('')
if (!hasKey) {
setApiKeyModal({ isOpen: true, model })
} else {
onModelChange(model)
onClose()
}
}, [isOpen])
}
const handleApiKeySave = (apiKey: string) => {
if (apiKeyModal.model) {
setApiKey(apiKeyModal.model.provider.id, apiKey)
setApiKeyStatuses((prev) => ({ ...prev, [apiKeyModal.model!.provider.id]: true }))
// Show provider models after API key is saved
const provider = apiKeyModal.model.provider
onModelChange(apiKeyModal.model)
setApiKeyModal({ isOpen: false, model: null })
setCurrentView('provider')
setSelectedProvider(provider)
onClose()
}
}
const handleClearApiKey = (providerId: string, e: React.MouseEvent) => {
e.stopPropagation()
clearApiKey(providerId as any)
clearApiKey(providerId)
setApiKeyStatuses((prev) => ({ ...prev, [providerId]: false }))
}
const handleProviderSelect = (provider: Provider) => {
if (provider.authType === 'oauth') {
// Check if already authenticated
if (hasApiKey(provider.id)) {
// Show provider models to select from
setCurrentView('provider')
setSelectedProvider(provider)
} else {
// Start OAuth flow
beginOAuthFlow(provider.id)
.then(() => {
// OAuth flow started successfully - modal will be shown via OAuth success handler
})
.catch((error) => {
console.error('Failed to start OAuth flow:', error)
alert('Failed to start authentication. Please try again.')
})
}
} else {
// Check if already has API key
if (hasApiKey(provider.id)) {
// Show provider models to select from
setCurrentView('provider')
setSelectedProvider(provider)
} else {
// Show API key modal first
setApiKeyModal({ isOpen: true, model: { provider } as Model })
}
}
}
const handleBackToMain = () => {
setCurrentView('main')
setSelectedProvider(null)
setSearchTerm('')
}
const getProviderModels = () => {
if (!selectedProvider) return []
return models.filter((model) => model.provider.id === selectedProvider.id)
}
const getFilteredModels = () => {
const providerModels = getProviderModels()
if (!searchTerm) return providerModels
return providerModels.filter((model) => model.name.toLowerCase().includes(searchTerm.toLowerCase()))
}
const handleModelSelect = (model: Model) => {
onModelChange(model)
addToFavorites(model.id)
onClose()
}
const getStatusIcon = (providerId: string) => {
const hasKey = apiKeyStatuses[providerId]
@ -175,211 +91,57 @@ const ModelSelectionModal: React.FC<ModelSelectionModalProps> = ({
style={{ backgroundColor: 'rgba(0,0,0,0.8)' }}
onClick={onClose}
>
<div
className="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] flex flex-col overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between p-6 border-b border-zinc-200 flex-shrink-0">
{currentView === 'provider' && selectedProvider ? (
<div className="flex items-center gap-3">
<button onClick={handleBackToMain} className="text-zinc-400 hover:text-zinc-600 p-1 cursor-pointer">
<ArrowLeft size={20} />
</button>
<span className="text-2xl">{selectedProvider.logo}</span>
<h2 className="text-xl font-semibold text-zinc-900">{selectedProvider.name} Models</h2>
</div>
) : (
<h2 className="text-xl font-semibold text-zinc-900">Select Model</h2>
)}
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] overflow-hidden" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between p-6 border-b border-zinc-200">
<h2 className="text-xl font-semibold text-zinc-900">Select Model</h2>
<button onClick={onClose} className="text-zinc-400 hover:text-zinc-600 p-1 cursor-pointer">
<X size={24} />
</button>
</div>
<div className="p-6 overflow-y-auto flex-1 min-h-0">
{currentView === 'main' ? (
<>
{/* Providers Section */}
<div className="mb-6">
<h3 className="text-lg font-medium text-zinc-900 mb-3">Providers</h3>
<div className="space-y-2">
{Object.values(providers).map((provider) => (
<div
key={provider.id}
className="border rounded-lg p-4 cursor-pointer transition-colors hover:border-zinc-300 hover:bg-zinc-50"
onClick={() => handleProviderSelect(provider)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-2xl">{provider.logo}</span>
<div>
<h4 className="font-medium text-zinc-900">{provider.name}</h4>
<p className="text-sm text-zinc-500">
{provider.authType === 'oauth' ? 'OAuth Authentication' : 'API Key Authentication'}
</p>
</div>
</div>
<div className="flex items-center gap-3">
{getStatusIcon(provider.id)}
{apiKeyStatuses[provider.id] && (
<button
onClick={(e) => handleClearApiKey(provider.id, e)}
className="text-zinc-400 hover:text-red-500 p-1"
title="Clear credentials"
>
<X size={16} />
</button>
)}
</div>
<div className="p-6 overflow-y-auto">
<div className="space-y-3">
{availableModels.map((model) => {
const isSelected = model.id === selectedModel.id
const hasKey = apiKeyStatuses[model.provider.id]
return (
<div
key={model.id}
className={`border rounded-lg p-4 cursor-pointer transition-colors ${
isSelected ? 'border-blue-500 bg-blue-50' : 'border-zinc-200 hover:border-zinc-300 hover:bg-zinc-50'
}`}
onClick={() => handleModelSelect(model)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-2xl">{model.provider.logo}</span>
<div>
<div className="font-medium text-zinc-900">{model.name}</div>
<div className="text-sm text-zinc-500">{model.provider.name}</div>
</div>
</div>
))}
</div>
</div>
{/* Starred Models Section */}
<div>
<h3 className="text-lg font-medium text-zinc-900 mb-3 flex items-center gap-2">
<Star size={20} fill="currentColor" className="text-yellow-500" />
Starred Models
</h3>
{getFavoriteModels().length === 0 ? (
<div className="text-center py-8 text-zinc-500">
<Star size={48} className="mx-auto mb-3 text-zinc-300" />
<p className="text-lg font-medium">No starred models yet</p>
<p className="text-sm">Click on a provider above to explore and star models</p>
</div>
) : (
<div className="space-y-2">
{getFavoriteModels().map((model) => {
const isConfigured = apiKeyStatuses[model.provider.id]
const isSelected = model.id === selectedModel.id
return (
<div
key={model.id}
className={`border rounded-lg p-4 cursor-pointer transition-colors ${
isSelected
? 'border-blue-500 bg-blue-50 ring-2 ring-blue-200'
: 'border-zinc-200 hover:border-zinc-300 hover:bg-zinc-50'
} ${!isConfigured ? 'opacity-50' : ''}`}
onClick={() => {
if (isConfigured) {
onModelChange(model)
addToFavorites(model.id)
} else {
handleProviderSelect(model.provider)
}
}}
<div className="flex items-center gap-2">
{getStatusIcon(model.provider.id)}
<span className={`text-sm ${hasKey ? 'text-green-600' : 'text-red-600'}`}>
{hasKey ? 'Configured' : 'Not configured'}
</span>
{hasKey && (
<button
onClick={(e) => handleClearApiKey(model.provider.id, e)}
className="text-zinc-400 hover:text-red-500 p-1 ml-2"
title="Clear API key"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Star size={16} fill="currentColor" className="text-yellow-500" />
{isSelected && <span className="text-blue-500">✓</span>}
<span className="text-lg">{model.provider.logo}</span>
<div>
<h4 className="font-medium text-zinc-900">{model.name}</h4>
<div className="text-sm text-zinc-500 flex items-center gap-2">
{model.provider.name}
{model.supportsTools && <span className="text-green-600">🔧</span>}
{model.reasoning && <span className="text-purple-600">🧠</span>}
{model.attachment && <span className="text-blue-600">📎</span>}
</div>
</div>
</div>
<div className="flex items-center gap-2">
{!isConfigured && (
<div title="Provider not configured">
<AlertCircle size={16} className="text-orange-500" />
</div>
)}
<button
onClick={(e) => {
e.stopPropagation()
toggleFavorite(model.id)
}}
className="text-yellow-500 hover:text-yellow-600 p-1"
title="Remove from favorites"
>
<Star size={16} fill="currentColor" />
</button>
</div>
</div>
</div>
)
})}
</div>
)}
</div>
</>
) : (
/* Provider Models View */
selectedProvider && (
<>
<div className="mb-4">
<p className="text-zinc-600 mb-4">Select models to add to your favorites</p>
{/* Search Bar */}
<div className="relative mb-4">
<Search size={20} className="absolute left-3 top-1/2 transform -translate-y-1/2 text-zinc-400" />
<input
type="text"
placeholder="Search models..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-zinc-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
{/* Models List */}
<div className="space-y-2">
{getFilteredModels().map((model) => (
<div
key={model.id}
className="border rounded-lg p-4 hover:border-zinc-300 hover:bg-zinc-50 transition-colors cursor-pointer"
onClick={() => handleModelSelect(model)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<button
onClick={(e) => {
e.stopPropagation()
toggleFavorite(model.id)
}}
className={`p-1 rounded ${isFavorite(model.id) ? 'text-yellow-500' : 'text-zinc-300 hover:text-yellow-500'}`}
title={isFavorite(model.id) ? 'Remove from favorites' : 'Add to favorites'}
>
<Star size={16} fill={isFavorite(model.id) ? 'currentColor' : 'none'} />
</button>
<div>
<h4 className="font-medium text-zinc-900">{model.name}</h4>
<div className="text-sm text-zinc-500 flex items-center gap-2">
{model.supportsTools && <span className="text-green-600">🔧 Tools</span>}
{model.reasoning && <span className="text-purple-600">🧠 Reasoning</span>}
{model.attachment && <span className="text-blue-600">📎 Attachments</span>}
<span>•</span>
<span>{(model.contextLimit / 1000).toFixed(0)}K context</span>
</div>
</div>
</div>
<div className="text-sm text-zinc-400">Click to select</div>
</div>
<X size={16} />
</button>
)}
</div>
))}
</div>
{getFilteredModels().length === 0 && (
<div className="text-center py-8 text-zinc-500">
<p>No models found matching "{searchTerm}"</p>
</div>
)}
<div className="mt-4 text-center text-sm text-zinc-500">
{getProviderModels().length} model{getProviderModels().length !== 1 ? 's' : ''} available
</div>
</>
)
)}
)
})}
</div>
</div>
</div>
</div>
@ -387,17 +149,7 @@ const ModelSelectionModal: React.FC<ModelSelectionModalProps> = ({
<ApiKeyModal
isOpen={apiKeyModal.isOpen}
onClose={() => setApiKeyModal({ isOpen: false, model: null })}
provider={
apiKeyModal.model?.provider ?? {
id: 'unknown' as any,
name: 'Unknown',
baseUrl: '',
logo: '',
documentationUrl: '',
authType: 'apiKey',
apiKeyHeader: '',
}
}
provider={apiKeyModal.model?.provider ?? { id: '', name: '', baseUrl: '', apiKeyHeader: '', documentationUrl: '' }}
onSave={handleApiKeySave}
/>
</>

Some files were not shown because too many files have changed in this diff Show more