From 438ceb333c30838ad193f1772257a3c12a040733 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Tue, 10 Jun 2025 17:37:11 +0100
Subject: [PATCH 01/33] Implement complete InferenceProvider system with
OpenRouter integration
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Features
- 🔧 InferenceProvider interface with pluggable architecture
- 🔑 OpenRouter API key and OAuth PKCE authentication providers
- 🛠️ Tool calling support with automatic agent loop execution
- 📱 Interactive test UI with model selection and chat interface
- 🎯 Provider-level filtering to tool-capable models only
- ⚛️ React context with proper state management for UI reactivity
## Components
- Core types and interfaces in src/types/inference.ts
- OpenRouter client with shared utilities for both auth methods
- Two separate providers (composition over inheritance):
- OpenRouterApiProvider for API key authentication
- OpenRouterOAuthProvider for OAuth PKCE flow
- React InferenceContext with useInference hook
- Interactive test UI with tool calling demonstration
- OAuth callback routing with proper namespacing
## Tool Calling
- 3 test tools: weather, calculator, time
- Automatic tool execution and agent loop
- Visual tool call display with arguments and results
- Error handling for malformed tool calls
## Bug Fixes
- Fixed model selection UI reactivity issue
- Fixed OAuth popup race condition causing "cancelled" errors
- Proper cleanup of event listeners and intervals
## Documentation
- Comprehensive interface design document
- Implementation strategy and key design decisions
- Testing approach and security considerations
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
.eslintrc.cjs | 19 +
.gitignore | 72 +
README.md | 65 +
docs/inference_provider_interface.md | 338 ++
index.html | 13 +
package-lock.json | 6104 ++++++++++++++++++++
package.json | 40 +
postcss.config.js | 6 +
src/App.tsx | 32 +
src/components/InferenceTest.tsx | 368 ++
src/components/OAuthCallback.tsx | 72 +
src/contexts/InferenceContext.tsx | 144 +
src/index.css | 38 +
src/main.tsx | 10 +
src/providers/index.ts | 3 +
src/providers/openrouter/api-provider.ts | 155 +
src/providers/openrouter/client.ts | 295 +
src/providers/openrouter/index.ts | 10 +
src/providers/openrouter/oauth-provider.ts | 356 ++
src/providers/openrouter/types.ts | 113 +
src/types/inference.ts | 109 +
src/utils/testTools.ts | 108 +
tailwind.config.js | 30 +
tsconfig.json | 31 +
tsconfig.node.json | 10 +
vite.config.ts | 23 +
26 files changed, 8564 insertions(+)
create mode 100644 .eslintrc.cjs
create mode 100644 README.md
create mode 100644 docs/inference_provider_interface.md
create mode 100644 index.html
create mode 100644 package-lock.json
create mode 100644 package.json
create mode 100644 postcss.config.js
create mode 100644 src/App.tsx
create mode 100644 src/components/InferenceTest.tsx
create mode 100644 src/components/OAuthCallback.tsx
create mode 100644 src/contexts/InferenceContext.tsx
create mode 100644 src/index.css
create mode 100644 src/main.tsx
create mode 100644 src/providers/index.ts
create mode 100644 src/providers/openrouter/api-provider.ts
create mode 100644 src/providers/openrouter/client.ts
create mode 100644 src/providers/openrouter/index.ts
create mode 100644 src/providers/openrouter/oauth-provider.ts
create mode 100644 src/providers/openrouter/types.ts
create mode 100644 src/types/inference.ts
create mode 100644 src/utils/testTools.ts
create mode 100644 tailwind.config.js
create mode 100644 tsconfig.json
create mode 100644 tsconfig.node.json
create mode 100644 vite.config.ts
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000..6d210a1
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,19 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2020: true },
+ extends: [
+ 'eslint:recommended',
+ '@typescript-eslint/recommended',
+ 'plugin:react-hooks/recommended',
+ ],
+ ignorePatterns: ['dist', '.eslintrc.cjs'],
+ parser: '@typescript-eslint/parser',
+ plugins: ['react-refresh'],
+ rules: {
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
+ },
+}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index ae65225..5c2e5d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,73 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+# Dependency directories
+node_modules/
+.pnp
+.pnp.js
+
+# Build outputs
+dist/
+dist-ssr/
+build/
+.next/
+out/
+
+# Environment variables
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Coverage directory used by tools like istanbul
+coverage/
+*.lcov
+.nyc_output/
+
+# Vite
+.vite/
+
+# Testing
+.vitest/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea/
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# OS generated files
+Thumbs.db
+ehthumbs.db
+
+# Temporary files
+*.tmp
+*.temp
+.cache/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Reference materials (keeping this as requested)
reference_info/
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..06701b3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,65 @@
+# Example Remote MCP Client
+
+A React TypeScript application for connecting to multiple MCP (Model Context Protocol) servers and providing a conversational interface with tool calling capabilities.
+
+## Features
+
+- 🔗 Multi-server MCP connections (HTTP/SSE transports)
+- 🤖 Inference provider abstraction (starting with OpenRouter)
+- 💬 Conversational interface with agent loops
+- 🛠️ Real-time tool call visualization
+- 🔍 MCP debugging and message tracing
+- 📱 Responsive UI with left sidebar and chat interface
+
+## Development
+
+### Prerequisites
+
+- Node.js (v18 or higher)
+- npm, yarn, or pnpm
+
+### Getting Started
+
+1. Install dependencies:
+ ```bash
+ npm install
+ ```
+
+2. Start the development server:
+ ```bash
+ npm run dev
+ ```
+
+3. Open [http://localhost:3000](http://localhost:3000) in your browser
+
+### Available Scripts
+
+- `npm run dev` - Start development server
+- `npm run build` - Build for production
+- `npm run preview` - Preview production build
+- `npm run lint` - Run ESLint
+- `npm run test` - Run tests
+- `npm run test:ui` - Run tests with UI
+
+## Architecture
+
+The application is built with a modular architecture using React hooks and providers:
+
+- **InferenceProvider** - Abstraction for LLM inference (OpenRouter, etc.)
+- **MCPProvider** - Multi-server MCP connection management
+- **AgentLoop** - Tool calling and conversation flow
+- **UI Components** - Modular, reusable interface components
+
+## Technology Stack
+
+- **React 18** with TypeScript
+- **Vite** for build tooling
+- **Tailwind CSS** for styling
+- **MCP TypeScript SDK** for protocol implementation
+- **Vitest** for testing
+
+## Project Status
+
+This is an active development project serving as both:
+- A public example implementation of MCP client features
+- A prototyping testbed for MCP protocol changes
\ No newline at end of file
diff --git a/docs/inference_provider_interface.md b/docs/inference_provider_interface.md
new file mode 100644
index 0000000..d2a4b3e
--- /dev/null
+++ b/docs/inference_provider_interface.md
@@ -0,0 +1,338 @@
+# InferenceProvider Interface Design
+
+This document outlines the design for the InferenceProvider interface and its implementations, which abstracts LLM inference capabilities for the MCP client.
+
+## Goals
+
+1. **Provider Agnostic**: Support multiple inference providers (OpenRouter, Anthropic, OpenAI, Google, etc.)
+2. **Tool Calling**: Native support for function/tool calling with MCP tools
+3. **Authentication**: Flexible auth patterns (API keys, OAuth, etc.)
+4. **Model Selection**: Dynamic model listing and selection
+5. **Error Handling**: Consistent error handling across providers
+
+## Core Interface
+
+```typescript
+interface InferenceProvider {
+ // Provider identification
+ readonly name: string;
+ readonly id: string;
+
+ // Authentication state
+ readonly isAuthenticated: boolean;
+ readonly authError?: string;
+
+ // Available models
+ readonly models: Model[];
+ readonly selectedModel?: Model;
+
+ // Core inference method
+ generateResponse(request: InferenceRequest): Promise;
+
+ // Model management
+ loadModels(): Promise;
+ selectModel(modelId: string): void;
+
+ // Authentication
+ authenticate(config: AuthConfig): Promise;
+ logout(): void;
+
+ // Provider-specific capabilities
+ getCapabilities(): ProviderCapabilities;
+}
+```
+
+## Supporting Types
+
+### Model
+```typescript
+interface Model {
+ id: string;
+ name: string;
+ description?: string;
+ contextLength: number;
+ inputCost?: number; // per token
+ outputCost?: number; // per token
+ provider: string;
+ capabilities: ModelCapabilities;
+}
+
+interface ModelCapabilities {
+ supportsTools: boolean;
+ supportsVision: boolean;
+ maxTokens: number;
+}
+```
+
+### Inference Request/Response
+```typescript
+interface InferenceRequest {
+ messages: ChatMessage[];
+ tools?: Tool[]; // toolChoice defaults to 'auto' when tools provided
+ maxTokens?: number;
+ temperature?: number;
+ stopSequences?: string[];
+}
+
+interface InferenceResponse {
+ message: ChatMessage;
+ usage: TokenUsage;
+ stopReason: 'stop' | 'max_tokens' | 'tool_calls' | 'error';
+ error?: string;
+}
+
+interface ChatMessage {
+ role: 'user' | 'assistant' | 'system' | 'tool';
+ content: string | ContentBlock[];
+ toolCalls?: ToolCall[];
+ toolCallId?: string; // for tool response messages
+}
+
+interface ToolCall {
+ id: string;
+ type: 'function';
+ function: {
+ name: string;
+ arguments: string; // JSON string
+ };
+}
+```
+
+### Authentication
+```typescript
+interface AuthConfig {
+ type: 'api_key' | 'oauth';
+ apiKey?: string;
+ oauthConfig?: {
+ clientId?: string;
+ redirectUri?: string;
+ scopes?: string[];
+ };
+}
+
+interface ProviderCapabilities {
+ authMethods: ('api_key' | 'oauth')[];
+ supportsModelListing: boolean;
+ supportsToolCalling: boolean;
+ requiresAuth: boolean;
+}
+```
+
+## Provider Implementations
+
+### OpenRouter Providers
+
+For simplicity, we implement two separate providers using composition:
+
+**OpenRouterApiProvider:**
+- API key authentication only
+- Simpler auth flow and error handling
+- Direct API access
+
+**OpenRouterOAuthProvider:**
+- OAuth PKCE authentication only
+- Browser-based auth popup flow
+- Token refresh handling
+
+**Shared Configuration:**
+```typescript
+interface OpenRouterBaseConfig {
+ baseUrl?: string; // defaults to https://openrouter.ai/api/v1
+ defaultModel?: string;
+ httpReferrer?: string;
+ appName?: string;
+}
+
+interface OpenRouterApiConfig extends OpenRouterBaseConfig {
+ apiKey: string;
+}
+
+interface OpenRouterOAuthConfig extends OpenRouterBaseConfig {
+ clientId?: string; // for custom OAuth apps
+ redirectUri?: string;
+}
+```
+
+**Shared Logic:**
+Both providers use composition with shared utilities:
+- `OpenRouterClient` - HTTP client and request formatting
+- `OpenRouterModelParser` - Model list parsing and capabilities
+- `OpenRouterToolFormatter` - Tool call formatting
+
+### Future Providers
+
+**AnthropicProvider:**
+- OAuth integration with Anthropic Console
+- Claude model family support
+- Message batching optimization
+
+**OpenAIProvider:**
+- OpenAI API key or OAuth
+- GPT model family support
+- Function calling optimization
+
+**GoogleProvider:**
+- Google Cloud OAuth
+- Gemini model family support
+- Tool calling with Google AI format
+
+## React Integration
+
+### Provider Context
+```typescript
+interface InferenceContextValue {
+ provider: InferenceProvider | null;
+ isLoading: boolean;
+ error: string | null;
+
+ // Actions
+ setProvider: (provider: InferenceProvider) => void;
+ clearProvider: () => void;
+ generateResponse: (request: InferenceRequest) => Promise;
+ selectModel: (modelId: string) => void;
+ loadModels: () => Promise;
+
+ // Convenience getters
+ models: Model[];
+ selectedModel: Model | undefined;
+ isAuthenticated: boolean;
+}
+
+const InferenceContext = createContext(null);
+```
+
+### Hook Usage
+```typescript
+const useInference = () => {
+ const context = useContext(InferenceContext);
+ if (!context) {
+ throw new Error('useInference must be used within InferenceProvider');
+ }
+ return context;
+};
+```
+
+## Error Handling
+
+### Error Types
+```typescript
+interface InferenceError {
+ type: 'auth' | 'network' | 'rate_limit' | 'invalid_request' | 'provider_error';
+ message: string;
+ details?: any;
+ retryable: boolean;
+}
+```
+
+### Error Scenarios
+- **Authentication failures**: Invalid API key, expired tokens
+- **Rate limiting**: Provider-specific rate limits
+- **Network errors**: Connection issues, timeouts
+- **Validation errors**: Invalid tool schemas, malformed requests
+- **Provider errors**: Service unavailable, model not found
+
+## Implementation Strategy
+
+### Phase 1: Basic OpenRouter Support ✅
+1. Implement core InferenceProvider interface
+2. Create OpenRouterApiProvider with API key auth
+3. Basic model listing and selection (filtered to tool-capable models only)
+4. Simple tool calling support (toolChoice=auto)
+5. React context with proper state management for UI reactivity
+
+### Phase 2: OAuth and Enhanced Features ✅
+1. Create OpenRouterOAuthProvider with PKCE auth
+2. Implement error recovery and retries
+3. Add usage tracking and cost estimation
+4. Improve tool call validation
+
+### Current Status
+- ✅ Two OpenRouter providers (API key & OAuth) implemented
+- ✅ Provider-level model filtering (only tool-capable models)
+- ✅ React context with proper UI state synchronization
+- ✅ Interactive test UI with tool calling demonstration
+- ✅ OAuth callback routing for future MCP server integration
+
+### Phase 3: Multi-Provider Support
+1. Add AnthropicProvider
+2. Add OpenAIProvider
+3. Provider switching UI
+4. Unified configuration management
+
+## Testing Strategy
+
+### Unit Tests
+- Mock provider implementations for testing
+- Tool call serialization/deserialization
+- Error handling scenarios
+- Authentication flows
+
+### Integration Tests
+- Real provider API integration (with test keys)
+- End-to-end tool calling workflows
+- Provider switching scenarios
+- Authentication persistence
+
+## Security Considerations
+
+1. **API Key Storage**: Secure storage in browser (encrypted localStorage)
+2. **OAuth Flow**: Secure popup-based OAuth with PKCE
+3. **Token Refresh**: Automatic token refresh handling
+4. **Rate Limiting**: Client-side rate limiting to prevent abuse
+5. **Input Validation**: Strict validation of tool calls and responses
+
+## Key Design Decisions
+
+### 1. Provider-Level Model Filtering
+All providers filter models to only return those that support tool calling. This simplifies consumers and prevents tool-calling errors:
+
+```typescript
+// In OpenRouterClient.fetchModels()
+return response.data
+ .filter(model => model.supported_parameters?.includes('tools'))
+ .map(this.parseModel);
+```
+
+**Benefits:**
+- No tool capability checking needed in UI components
+- Eliminates "model doesn't support tools" errors
+- Cleaner separation of concerns
+
+### 2. React State Management for UI Reactivity
+The React context tracks model selection in React state to ensure UI updates when models are selected:
+
+```typescript
+const [selectedModelId, setSelectedModelId] = useState();
+
+// In selectModel callback
+provider.selectModel(modelId); // Update provider internal state
+setSelectedModelId(modelId); // Update React state → triggers re-render
+
+// In context value - compute selectedModel from React-tracked ID
+selectedModel: selectedModelId ? provider?.models.find(m => m.id === selectedModelId) : undefined
+```
+
+**Why this pattern:**
+- Provider internal state changes don't trigger React re-renders automatically
+- React needs to track state changes to update the UI
+- Keeps provider logic decoupled from React specifics
+
+### 3. Composition Over Inheritance
+Two separate OpenRouter providers (API & OAuth) use shared utilities rather than complex inheritance:
+- `OpenRouterClient` - HTTP client and request formatting
+- Shared model parsing and tool formatting logic
+- Clean separation of authentication concerns
+
+**Benefits:**
+- Simpler code paths (no branching auth logic)
+- Easier testing and maintenance
+- Clear separation of API vs OAuth concerns
+
+## Future Enhancements
+
+1. **Caching**: Response caching for repeated requests
+2. **Load Balancing**: Multi-provider load balancing
+3. **Analytics**: Usage analytics and monitoring
+4. **Custom Models**: Support for custom/fine-tuned models
+5. **Advanced Tool Choice**: Support for specific tool selection (beyond auto)
+6. **Streaming Support**: Real-time response streaming (if needed later)
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..914976a
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Example Remote MCP Client
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..cee0e06
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,6104 @@
+{
+ "name": "example-remote-client",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "example-remote-client",
+ "version": "0.0.0",
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.0.0",
+ "clsx": "^2.0.0",
+ "lucide-react": "^0.344.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "uuid": "^10.0.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.55",
+ "@types/react-dom": "^18.2.19",
+ "@types/uuid": "^10.0.0",
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
+ "@typescript-eslint/parser": "^6.21.0",
+ "@vitejs/plugin-react": "^4.2.1",
+ "@vitest/ui": "^1.2.0",
+ "autoprefixer": "^10.4.17",
+ "eslint": "^8.56.0",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.5",
+ "postcss": "^8.4.35",
+ "tailwindcss": "^3.4.1",
+ "typescript": "^5.2.2",
+ "vite": "^5.1.0",
+ "vitest": "^1.2.0"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.27.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz",
+ "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.27.4",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
+ "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.27.3",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.27.3",
+ "@babel/helpers": "^7.27.4",
+ "@babel/parser": "^7.27.4",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.27.4",
+ "@babel/types": "^7.27.3",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.27.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz",
+ "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.27.5",
+ "@babel/types": "^7.27.3",
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.27.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
+ "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.27.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.27.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
+ "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.27.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.27.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
+ "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.3"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.27.4",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz",
+ "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.27.3",
+ "@babel/parser": "^7.27.4",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.27.3",
+ "debug": "^4.3.1",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.27.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
+ "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
+ "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
+ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
+ "deprecated": "Use @eslint/config-array instead",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^2.0.3",
+ "debug": "^4.3.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
+ "deprecated": "Use @eslint/object-schema instead",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
+ "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+ "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.1.tgz",
+ "integrity": "sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.6",
+ "content-type": "^1.0.5",
+ "cors": "^2.8.5",
+ "cross-spawn": "^7.0.5",
+ "eventsource": "^3.0.2",
+ "express": "^5.0.1",
+ "express-rate-limit": "^7.5.0",
+ "pkce-challenge": "^5.0.0",
+ "raw-body": "^3.0.0",
+ "zod": "^3.23.8",
+ "zod-to-json-schema": "^3.24.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@polka/url": {
+ "version": "1.0.0-next.29",
+ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
+ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz",
+ "integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.42.0.tgz",
+ "integrity": "sha512-gldmAyS9hpj+H6LpRNlcjQWbuKUtb94lodB9uCz71Jm+7BxK1VIOo7y62tZZwxhA7j1ylv/yQz080L5WkS+LoQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.42.0.tgz",
+ "integrity": "sha512-bpRipfTgmGFdCZDFLRvIkSNO1/3RGS74aWkJJTFJBH7h3MRV4UijkaEUeOMbi9wxtxYmtAbVcnMtHTPBhLEkaw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.42.0.tgz",
+ "integrity": "sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.42.0.tgz",
+ "integrity": "sha512-rv5UZaWVIJTDMyQ3dCEK+m0SAn6G7H3PRc2AZmExvbDvtaDc+qXkei0knQWcI3+c9tEs7iL/4I4pTQoPbNL2SA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.42.0.tgz",
+ "integrity": "sha512-fJcN4uSGPWdpVmvLuMtALUFwCHgb2XiQjuECkHT3lWLZhSQ3MBQ9pq+WoWeJq2PrNxr9rPM1Qx+IjyGj8/c6zQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.42.0.tgz",
+ "integrity": "sha512-CziHfyzpp8hJpCVE/ZdTizw58gr+m7Y2Xq5VOuCSrZR++th2xWAz4Nqk52MoIIrV3JHtVBhbBsJcAxs6NammOQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.42.0.tgz",
+ "integrity": "sha512-UsQD5fyLWm2Fe5CDM7VPYAo+UC7+2Px4Y+N3AcPh/LdZu23YcuGPegQly++XEVaC8XUTFVPscl5y5Cl1twEI4A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.42.0.tgz",
+ "integrity": "sha512-/i8NIrlgc/+4n1lnoWl1zgH7Uo0XK5xK3EDqVTf38KvyYgCU/Rm04+o1VvvzJZnVS5/cWSd07owkzcVasgfIkQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.42.0.tgz",
+ "integrity": "sha512-eoujJFOvoIBjZEi9hJnXAbWg+Vo1Ov8n/0IKZZcPZ7JhBzxh2A+2NFyeMZIRkY9iwBvSjloKgcvnjTbGKHE44Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.42.0.tgz",
+ "integrity": "sha512-/3NrcOWFSR7RQUQIuZQChLND36aTU9IYE4j+TB40VU78S+RA0IiqHR30oSh6P1S9f9/wVOenHQnacs/Byb824g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.42.0.tgz",
+ "integrity": "sha512-O8AplvIeavK5ABmZlKBq9/STdZlnQo7Sle0LLhVA7QT+CiGpNVe197/t8Aph9bhJqbDVGCHpY2i7QyfEDDStDg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.42.0.tgz",
+ "integrity": "sha512-6Qb66tbKVN7VyQrekhEzbHRxXXFFD8QKiFAwX5v9Xt6FiJ3BnCVBuyBxa2fkFGqxOCSGGYNejxd8ht+q5SnmtA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.42.0.tgz",
+ "integrity": "sha512-KQETDSEBamQFvg/d8jajtRwLNBlGc3aKpaGiP/LvEbnmVUKlFta1vqJqTrvPtsYsfbE/DLg5CC9zyXRX3fnBiA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.42.0.tgz",
+ "integrity": "sha512-qMvnyjcU37sCo/tuC+JqeDKSuukGAd+pVlRl/oyDbkvPJ3awk6G6ua7tyum02O3lI+fio+eM5wsVd66X0jQtxw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.42.0.tgz",
+ "integrity": "sha512-I2Y1ZUgTgU2RLddUHXTIgyrdOwljjkmcZ/VilvaEumtS3Fkuhbw4p4hgHc39Ypwvo2o7sBFNl2MquNvGCa55Iw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.42.0.tgz",
+ "integrity": "sha512-Gfm6cV6mj3hCUY8TqWa63DB8Mx3NADoFwiJrMpoZ1uESbK8FQV3LXkhfry+8bOniq9pqY1OdsjFWNsSbfjPugw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.42.0.tgz",
+ "integrity": "sha512-g86PF8YZ9GRqkdi0VoGlcDUb4rYtQKyTD1IVtxxN4Hpe7YqLBShA7oHMKU6oKTCi3uxwW4VkIGnOaH/El8de3w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.42.0.tgz",
+ "integrity": "sha512-+axkdyDGSp6hjyzQ5m1pgcvQScfHnMCcsXkx8pTgy/6qBmWVhtRVlgxjWwDp67wEXXUr0x+vD6tp5W4x6V7u1A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.42.0.tgz",
+ "integrity": "sha512-F+5J9pelstXKwRSDq92J0TEBXn2nfUrQGg+HK1+Tk7VOL09e0gBqUHugZv7SW4MGrYj41oNCUe3IKCDGVlis2g==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.42.0.tgz",
+ "integrity": "sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
+ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.20.7",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
+ "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.20.7"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.23",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
+ "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@types/semver": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz",
+ "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
+ "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.5.1",
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/type-utils": "6.21.0",
+ "@typescript-eslint/utils": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
+ "debug": "^4.3.4",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.4",
+ "natural-compare": "^1.4.0",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
+ "eslint": "^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
+ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/typescript-estree": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
+ "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz",
+ "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "6.21.0",
+ "@typescript-eslint/utils": "6.21.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
+ "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
+ "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "9.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
+ "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@types/json-schema": "^7.0.12",
+ "@types/semver": "^7.5.0",
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/typescript-estree": "6.21.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
+ "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.2.tgz",
+ "integrity": "sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.27.4",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.11",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz",
+ "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "1.6.1",
+ "@vitest/utils": "1.6.1",
+ "chai": "^4.3.10"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz",
+ "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "1.6.1",
+ "p-limit": "^5.0.0",
+ "pathe": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner/node_modules/p-limit": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
+ "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@vitest/runner/node_modules/yocto-queue": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz",
+ "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz",
+ "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "magic-string": "^0.30.5",
+ "pathe": "^1.1.1",
+ "pretty-format": "^29.7.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz",
+ "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^2.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/ui": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-1.6.1.tgz",
+ "integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "1.6.1",
+ "fast-glob": "^3.3.2",
+ "fflate": "^0.8.1",
+ "flatted": "^3.2.9",
+ "pathe": "^1.1.1",
+ "picocolors": "^1.0.0",
+ "sirv": "^2.0.4"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "vitest": "1.6.1"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz",
+ "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "diff-sequences": "^29.6.3",
+ "estree-walker": "^3.0.3",
+ "loupe": "^2.3.7",
+ "pretty-format": "^29.7.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.4",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+ "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.21",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
+ "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.24.4",
+ "caniuse-lite": "^1.0.30001702",
+ "fraction.js": "^4.3.7",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
+ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.0",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.6.3",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.0",
+ "raw-body": "^3.0.0",
+ "type-is": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.25.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
+ "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001718",
+ "electron-to-chromium": "^1.5.160",
+ "node-releases": "^2.0.19",
+ "update-browserslist-db": "^1.1.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001721",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz",
+ "integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chai": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
+ "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^1.1.0",
+ "check-error": "^1.0.3",
+ "deep-eql": "^4.1.3",
+ "get-func-name": "^2.0.2",
+ "loupe": "^2.3.6",
+ "pathval": "^1.1.1",
+ "type-detect": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
+ "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-func-name": "^2.0.2"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/confbox": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/content-disposition": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
+ "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-eql": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
+ "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-detect": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/diff-sequences": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
+ "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.166",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.166.tgz",
+ "integrity": "sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
+ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
+ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.57.1",
+ "@humanwhocodes/config-array": "^0.13.0",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
+ "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.20",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz",
+ "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventsource": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
+ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
+ "license": "MIT",
+ "dependencies": {
+ "eventsource-parser": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/eventsource-parser": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.2.tgz",
+ "integrity": "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/execa": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
+ "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^8.0.1",
+ "human-signals": "^5.0.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-final-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=16.17"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
+ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.0",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-rate-limit": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
+ "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/express-rate-limit"
+ },
+ "peerDependencies": {
+ "express": "^4.11 || 5 || ^5.0.0-beta.1"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
+ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-func-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
+ "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
+ "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-errors/node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
+ "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=16.17.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/local-pkg": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz",
+ "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mlly": "^1.7.3",
+ "pkg-types": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
+ "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-func-name": "^2.0.1"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.344.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.344.0.tgz",
+ "integrity": "sha512-6YyBnn91GB45VuVT96bYCOKElbJzUHqp65vX8cDcu55MQL9T969v4dhGClpljamuI/+KMO9P6w9Acq1CVQGvIQ==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.17",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+ "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/mlly": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
+ "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "pathe": "^2.0.1",
+ "pkg-types": "^1.3.0",
+ "ufo": "^1.5.4"
+ }
+ },
+ "node_modules/mlly/node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mrmime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+ "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
+ "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
+ "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
+ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/pkce-challenge": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
+ "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/pkg-types": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+ "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "confbox": "^0.1.8",
+ "mlly": "^1.7.4",
+ "pathe": "^2.0.1"
+ }
+ },
+ "node_modules/pkg-types/node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.4",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz",
+ "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+ "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+ "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.0.0",
+ "yaml": "^2.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "postcss": ">=8.0.9",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
+ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
+ "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.6.3",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.10",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+ "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.42.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.42.0.tgz",
+ "integrity": "sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.7"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.42.0",
+ "@rollup/rollup-android-arm64": "4.42.0",
+ "@rollup/rollup-darwin-arm64": "4.42.0",
+ "@rollup/rollup-darwin-x64": "4.42.0",
+ "@rollup/rollup-freebsd-arm64": "4.42.0",
+ "@rollup/rollup-freebsd-x64": "4.42.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.42.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.42.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.42.0",
+ "@rollup/rollup-linux-arm64-musl": "4.42.0",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.42.0",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.42.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.42.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.42.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.42.0",
+ "@rollup/rollup-linux-x64-gnu": "4.42.0",
+ "@rollup/rollup-linux-x64-musl": "4.42.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.42.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.42.0",
+ "@rollup/rollup-win32-x64-msvc": "4.42.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rollup/node_modules/@types/estree": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
+ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
+ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.5",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "mime-types": "^3.0.1",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/sirv": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
+ "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@polka/url": "^1.0.0-next.24",
+ "mrmime": "^2.0.0",
+ "totalist": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/std-env": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
+ "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/string-width/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-literal": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz",
+ "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^9.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/strip-literal/node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.0",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
+ "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "glob": "^10.3.10",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/sucrase/node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/sucrase/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.17",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
+ "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.6",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinypool": {
+ "version": "0.8.4",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz",
+ "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
+ "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/totalist": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
+ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
+ "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
+ "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/ufo": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
+ "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+ "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.19",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
+ "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz",
+ "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.3.4",
+ "pathe": "^1.1.1",
+ "picocolors": "^1.0.0",
+ "vite": "^5.0.0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz",
+ "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "1.6.1",
+ "@vitest/runner": "1.6.1",
+ "@vitest/snapshot": "1.6.1",
+ "@vitest/spy": "1.6.1",
+ "@vitest/utils": "1.6.1",
+ "acorn-walk": "^8.3.2",
+ "chai": "^4.3.10",
+ "debug": "^4.3.4",
+ "execa": "^8.0.1",
+ "local-pkg": "^0.5.0",
+ "magic-string": "^0.30.5",
+ "pathe": "^1.1.1",
+ "picocolors": "^1.0.0",
+ "std-env": "^3.5.0",
+ "strip-literal": "^2.0.0",
+ "tinybench": "^2.5.1",
+ "tinypool": "^0.8.3",
+ "vite": "^5.0.0",
+ "vite-node": "1.6.1",
+ "why-is-node-running": "^2.2.2"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "@vitest/browser": "1.6.1",
+ "@vitest/ui": "1.6.1",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yaml": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
+ "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.25.57",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.57.tgz",
+ "integrity": "sha512-6tgzLuwVST5oLUxXTmBqoinKMd3JeesgbgseXeFasKKj8Q1FCZrHnbqJOyiEvr4cVAlbug+CgIsmJ8cl/pU5FA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-to-json-schema": {
+ "version": "3.24.5",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz",
+ "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==",
+ "license": "ISC",
+ "peerDependencies": {
+ "zod": "^3.24.1"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..1217b79
--- /dev/null
+++ b/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "example-remote-client",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview",
+ "test": "vitest",
+ "test:ui": "vitest --ui"
+ },
+ "dependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "@modelcontextprotocol/sdk": "^1.0.0",
+ "uuid": "^10.0.0",
+ "clsx": "^2.0.0",
+ "lucide-react": "^0.344.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.55",
+ "@types/react-dom": "^18.2.19",
+ "@types/uuid": "^10.0.0",
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
+ "@typescript-eslint/parser": "^6.21.0",
+ "@vitejs/plugin-react": "^4.2.1",
+ "autoprefixer": "^10.4.17",
+ "eslint": "^8.56.0",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.5",
+ "postcss": "^8.4.35",
+ "tailwindcss": "^3.4.1",
+ "typescript": "^5.2.2",
+ "vite": "^5.1.0",
+ "vitest": "^1.2.0",
+ "@vitest/ui": "^1.2.0"
+ }
+}
\ No newline at end of file
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..e99ebc2
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..d269034
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,32 @@
+import React from 'react'
+import { InferenceProvider } from '@/contexts/InferenceContext'
+import { InferenceTest } from '@/components/InferenceTest'
+import { OAuthCallback } from '@/components/OAuthCallback'
+
+function App() {
+ // Simple routing based on pathname
+ const pathname = window.location.pathname;
+ const isInferenceOAuthCallback = pathname === '/oauth/inference/callback';
+ const isMcpOAuthCallback = pathname.startsWith('/oauth/mcp/');
+
+ if (isInferenceOAuthCallback) {
+ return ;
+ }
+
+ if (isMcpOAuthCallback) {
+ // Extract server identifier from path like /oauth/mcp/server123/callback
+ const serverMatch = pathname.match(/^\/oauth\/mcp\/([^\/]+)\/callback$/);
+ const serverId = serverMatch?.[1];
+ return ;
+ }
+
+ return (
+
+
+
+
+
+ )
+}
+
+export default App
\ No newline at end of file
diff --git a/src/components/InferenceTest.tsx b/src/components/InferenceTest.tsx
new file mode 100644
index 0000000..d246ed0
--- /dev/null
+++ b/src/components/InferenceTest.tsx
@@ -0,0 +1,368 @@
+// Test UI for inference provider functionality
+
+import React, { useState, useCallback } from 'react';
+import { useInference } from '@/contexts/InferenceContext';
+import { OpenRouterApiProvider, OpenRouterOAuthProvider } from '@/providers/openrouter';
+import type { ChatMessage, InferenceRequest } from '@/types/inference';
+import { testTools, executeTestTool } from '@/utils/testTools';
+
+export function InferenceTest() {
+ const {
+ provider,
+ isLoading,
+ error,
+ setProvider,
+ clearProvider,
+ generateResponse,
+ selectModel,
+ loadModels,
+ models,
+ selectedModel,
+ isAuthenticated,
+ } = useInference();
+
+ const [apiKey, setApiKey] = useState('');
+ const [message, setMessage] = useState('');
+ const [response, setResponse] = useState('');
+ const [conversation, setConversation] = useState([]);
+ const [enableTools, setEnableTools] = useState(false);
+
+ const handleApiAuth = useCallback(async () => {
+ if (!apiKey.trim()) {
+ alert('Please enter an API key');
+ return;
+ }
+
+ try {
+ const apiProvider = new OpenRouterApiProvider();
+ await apiProvider.authenticate({ type: 'api_key', apiKey: apiKey.trim() });
+ setProvider(apiProvider);
+ } catch (err) {
+ console.error('API authentication failed:', err);
+ alert(`Authentication failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
+ }
+ }, [apiKey, setProvider]);
+
+ const handleOAuthAuth = useCallback(async () => {
+ try {
+ const oauthProvider = new OpenRouterOAuthProvider();
+ await oauthProvider.authenticate({ type: 'oauth' });
+ setProvider(oauthProvider);
+ } catch (err) {
+ console.error('OAuth authentication failed:', err);
+ alert(`OAuth failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
+ }
+ }, [setProvider]);
+
+ const handleLoadModels = useCallback(async () => {
+ try {
+ await loadModels();
+ } catch (err) {
+ console.error('Failed to load models:', err);
+ alert(`Failed to load models: ${err instanceof Error ? err.message : 'Unknown error'}`);
+ }
+ }, [loadModels]);
+
+ const handleSendMessage = useCallback(async () => {
+ if (!message.trim() || !provider || !selectedModel) {
+ alert('Please enter a message and ensure a model is selected');
+ return;
+ }
+
+ const userMessage: ChatMessage = {
+ role: 'user',
+ content: message.trim(),
+ };
+
+ let currentConversation = [...conversation, userMessage];
+ setConversation(currentConversation);
+ setMessage('');
+ setResponse('');
+
+ const request: InferenceRequest = {
+ messages: currentConversation,
+ maxTokens: 500,
+ temperature: 0.7,
+ tools: enableTools ? testTools : undefined,
+ };
+
+ try {
+ const result = await generateResponse(request);
+ let assistantMessage = result.message;
+
+ // Handle tool calls
+ if (assistantMessage.toolCalls && assistantMessage.toolCalls.length > 0) {
+ // Add the assistant message with tool calls
+ currentConversation = [...currentConversation, assistantMessage];
+ setConversation(currentConversation);
+
+ // Execute each tool call and add tool results
+ for (const toolCall of assistantMessage.toolCalls) {
+ const toolResult = executeTestTool(toolCall.function.name, toolCall.function.arguments);
+
+ const toolMessage: ChatMessage = {
+ role: 'tool',
+ content: toolResult,
+ toolCallId: toolCall.id,
+ };
+
+ currentConversation = [...currentConversation, toolMessage];
+ setConversation(currentConversation);
+ }
+
+ // Make another request with the tool results
+ const followUpRequest: InferenceRequest = {
+ messages: currentConversation,
+ maxTokens: 500,
+ temperature: 0.7,
+ tools: enableTools ? testTools : undefined,
+ };
+
+ const followUpResult = await generateResponse(followUpRequest);
+ assistantMessage = followUpResult.message;
+ setResponse(JSON.stringify(followUpResult, null, 2));
+ } else {
+ setResponse(JSON.stringify(result, null, 2));
+ }
+
+ setConversation([...currentConversation, assistantMessage]);
+ } catch (err) {
+ console.error('Inference failed:', err);
+ alert(`Inference failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
+ }
+ }, [message, provider, selectedModel, conversation, generateResponse, enableTools]);
+
+ const handleClearConversation = useCallback(() => {
+ setConversation([]);
+ setResponse('');
+ }, []);
+
+ return (
+
+
+
+ Inference Provider Test
+
+
+ {/* Authentication Section */}
+
+
+ Authentication
+
+
+ {!isAuthenticated ? (
+
+ {/* API Key Auth */}
+
+ setApiKey(e.target.value)}
+ />
+
+ API Key Auth
+
+
+
+ {/* OAuth Auth */}
+
+
+ OAuth Auth
+
+
+
+ ) : (
+
+
+ ✓ Authenticated with {provider?.name}
+
+
+ Logout
+
+
+ )}
+
+
+ {/* Model Selection */}
+ {isAuthenticated && (
+
+
+
+ Model Selection
+
+
+ Reload Models
+
+
+
+ {models.length > 0 ? (
+
+
selectModel(e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
+ >
+ Select a model
+ {models.map((model) => (
+
+ {model.name} ({model.id})
+
+ ))}
+
+
+ {selectedModel && (
+
+ Context: {selectedModel.contextLength} tokens |
+ Max output: {selectedModel.capabilities.maxTokens} tokens |
+ Tools: ✓ (all models support tools)
+
+ )}
+
+ ) : (
+
No models loaded
+ )}
+
+ )}
+
+ {/* Chat Interface */}
+ {isAuthenticated && selectedModel && (
+
+
+
+ Chat Test
+
+
+ setEnableTools(e.target.checked)}
+ className="rounded"
+ />
+ Enable Tools
+
+
+ Clear
+
+
+
+ {/* Tool Test Suggestions */}
+ {enableTools && (
+
+
+ Tool Test Suggestions:
+
+
+
• "What's the weather in San Francisco?"
+
• "Calculate 123 + 456"
+
• "What time is it in London?"
+
• "Get weather for Tokyo and add 10 + 20"
+
+
+ Available tools: get_weather, calculate_sum, get_current_time
+
+
+ )}
+
+ {/* Conversation */}
+ {conversation.length > 0 && (
+
+ {conversation.map((msg, index) => (
+
+
+ {msg.role === 'user' && '👤 You'}
+ {msg.role === 'assistant' && '🤖 Assistant'}
+ {msg.role === 'tool' && '🛠️ Tool Result'}
+
+
+
+ {typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)}
+
+
+ {msg.toolCalls && msg.toolCalls.length > 0 && (
+
+ {msg.toolCalls.map((toolCall, tcIndex) => (
+
+
+ 🔧 {toolCall.function.name}
+
+
+ {JSON.stringify(toolCall.function.arguments, null, 2)}
+
+
+ ))}
+
+ )}
+
+ {msg.toolCallId && (
+
+ ↳ Response to tool call: {msg.toolCallId}
+
+ )}
+
+ ))}
+
+ )}
+
+ {/* Message Input */}
+
+ setMessage(e.target.value)}
+ onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
+ />
+
+ {isLoading ? 'Sending...' : 'Send'}
+
+
+
+ )}
+
+ {/* Error Display */}
+ {error && (
+
+ )}
+
+ {/* Response Debug */}
+ {response && (
+
+
+ Last Response (Debug):
+
+
+ {response}
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/OAuthCallback.tsx b/src/components/OAuthCallback.tsx
new file mode 100644
index 0000000..fd69cb8
--- /dev/null
+++ b/src/components/OAuthCallback.tsx
@@ -0,0 +1,72 @@
+// OAuth callback handler for popup-based OAuth flows
+
+import React, { useEffect } from 'react';
+
+interface OAuthCallbackProps {
+ type: 'inference' | 'mcp';
+ serverId?: string; // Required when type is 'mcp'
+}
+
+export function OAuthCallback({ type, serverId }: OAuthCallbackProps) {
+ useEffect(() => {
+ // Extract OAuth parameters from URL
+ const urlParams = new URLSearchParams(window.location.search);
+ const code = urlParams.get('code');
+ const state = urlParams.get('state');
+ const error = urlParams.get('error');
+ const errorDescription = urlParams.get('error_description');
+
+ // Send result to parent window
+ if (window.opener) {
+ const messageType = type === 'inference' ? 'oauth_callback' : 'mcp_oauth_callback';
+
+ if (error) {
+ window.opener.postMessage({
+ type: messageType,
+ callbackType: type,
+ serverId,
+ error: errorDescription || error,
+ }, window.location.origin);
+ } else if (code && state) {
+ window.opener.postMessage({
+ type: messageType,
+ callbackType: type,
+ serverId,
+ code,
+ state,
+ }, window.location.origin);
+ } else {
+ window.opener.postMessage({
+ type: messageType,
+ callbackType: type,
+ serverId,
+ error: 'Invalid OAuth callback - missing code or state',
+ }, window.location.origin);
+ }
+
+ // Close the popup after a small delay to ensure message is processed
+ setTimeout(() => {
+ window.close();
+ }, 100);
+ } else {
+ // Fallback if not in a popup - redirect to main app
+ window.location.href = '/';
+ }
+ }, []);
+
+ return (
+
+
+
+
+
+ Processing OAuth callback...
+
+
+ This window will close automatically.
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/contexts/InferenceContext.tsx b/src/contexts/InferenceContext.tsx
new file mode 100644
index 0000000..b82f9ec
--- /dev/null
+++ b/src/contexts/InferenceContext.tsx
@@ -0,0 +1,144 @@
+// React context for inference provider management
+
+import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
+import type {
+ InferenceProvider,
+ InferenceRequest,
+ InferenceResponse,
+ Model,
+} from '@/types/inference';
+
+interface InferenceContextValue {
+ // Current provider state
+ provider: InferenceProvider | null;
+ isLoading: boolean;
+ error: string | null;
+
+ // Provider actions
+ setProvider: (provider: InferenceProvider) => void;
+ clearProvider: () => void;
+
+ // Inference actions
+ generateResponse: (request: InferenceRequest) => Promise;
+ selectModel: (modelId: string) => void;
+ loadModels: () => Promise;
+
+ // Convenience getters
+ models: Model[];
+ selectedModel: Model | undefined;
+ isAuthenticated: boolean;
+}
+
+const InferenceContext = createContext(null);
+
+interface InferenceProviderProps {
+ children: ReactNode;
+}
+
+export function InferenceProvider({ children }: InferenceProviderProps) {
+ const [provider, setProviderState] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [selectedModelId, setSelectedModelId] = useState(undefined);
+
+ const setProvider = useCallback((newProvider: InferenceProvider) => {
+ setProviderState(newProvider);
+ setSelectedModelId(undefined);
+ setError(null);
+ }, []);
+
+ const clearProvider = useCallback(() => {
+ if (provider) {
+ provider.logout();
+ }
+ setProviderState(null);
+ setSelectedModelId(undefined);
+ setError(null);
+ }, [provider]);
+
+ const generateResponse = useCallback(async (request: InferenceRequest): Promise => {
+ if (!provider) {
+ throw new Error('No inference provider configured');
+ }
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const response = await provider.generateResponse(request);
+ return response;
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : 'Inference request failed';
+ setError(errorMessage);
+ throw err;
+ } finally {
+ setIsLoading(false);
+ }
+ }, [provider]);
+
+ const selectModel = useCallback((modelId: string) => {
+ if (!provider) {
+ throw new Error('No inference provider configured');
+ }
+
+ try {
+ provider.selectModel(modelId);
+ setSelectedModelId(modelId);
+ setError(null);
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : 'Failed to select model';
+ setError(errorMessage);
+ throw err;
+ }
+ }, [provider]);
+
+ const loadModels = useCallback(async (): Promise => {
+ if (!provider) {
+ throw new Error('No inference provider configured');
+ }
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const models = await provider.loadModels();
+ // Update selectedModelId to match what the provider selected as default
+ setSelectedModelId(provider.selectedModel?.id);
+ return models;
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : 'Failed to load models';
+ setError(errorMessage);
+ throw err;
+ } finally {
+ setIsLoading(false);
+ }
+ }, [provider]);
+
+ const contextValue: InferenceContextValue = {
+ provider,
+ isLoading,
+ error,
+ setProvider,
+ clearProvider,
+ generateResponse,
+ selectModel,
+ loadModels,
+ models: provider?.models || [],
+ selectedModel: selectedModelId ? provider?.models.find(m => m.id === selectedModelId) : undefined,
+ isAuthenticated: provider?.isAuthenticated || false,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useInference(): InferenceContextValue {
+ const context = useContext(InferenceContext);
+ if (!context) {
+ throw new Error('useInference must be used within an InferenceProvider');
+ }
+ return context;
+}
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..89fbc5d
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,38 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+}
\ No newline at end of file
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..cbe1cdf
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.tsx'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
\ No newline at end of file
diff --git a/src/providers/index.ts b/src/providers/index.ts
new file mode 100644
index 0000000..aede0f8
--- /dev/null
+++ b/src/providers/index.ts
@@ -0,0 +1,3 @@
+// All inference providers
+
+export * from './openrouter';
\ No newline at end of file
diff --git a/src/providers/openrouter/api-provider.ts b/src/providers/openrouter/api-provider.ts
new file mode 100644
index 0000000..ee444f8
--- /dev/null
+++ b/src/providers/openrouter/api-provider.ts
@@ -0,0 +1,155 @@
+// OpenRouter API Key Provider
+
+import { InferenceProvider } from '@/types/inference';
+import type {
+ Model,
+ InferenceRequest,
+ InferenceResponse,
+ AuthConfig,
+ ProviderCapabilities,
+ InferenceError,
+} from '@/types/inference';
+import { OpenRouterClient } from './client';
+import type { OpenRouterApiConfig } from './types';
+
+export class OpenRouterApiProvider extends InferenceProvider {
+ readonly name = 'OpenRouter (API Key)';
+ readonly id = 'openrouter-api';
+
+ private client: OpenRouterClient;
+ private apiKey?: string;
+ private _models: Model[] = [];
+ private _selectedModel?: Model;
+ private _authError?: string;
+
+ constructor(config?: Partial) {
+ super();
+ this.client = new OpenRouterClient(config || {});
+ if (config?.apiKey) {
+ this.apiKey = config.apiKey;
+ }
+ if (config?.defaultModel) {
+ // We'll set this after loading models
+ }
+ }
+
+ get isAuthenticated(): boolean {
+ return !!this.apiKey && !this._authError;
+ }
+
+ get authError(): string | undefined {
+ return this._authError;
+ }
+
+ get models(): Model[] {
+ return this._models;
+ }
+
+ get selectedModel(): Model | undefined {
+ return this._selectedModel;
+ }
+
+ async authenticate(config: AuthConfig): Promise {
+ if (config.type !== 'api_key' || !config.apiKey) {
+ throw new Error('OpenRouterApiProvider requires API key authentication');
+ }
+
+ this.apiKey = config.apiKey;
+ this._authError = undefined;
+
+ try {
+ // Test the API key by loading models
+ await this.loadModels();
+ } catch (error) {
+ this._authError = error instanceof Error ? error.message : 'Authentication failed';
+ this.apiKey = undefined;
+ throw error;
+ }
+ }
+
+ logout(): void {
+ this.apiKey = undefined;
+ this._authError = undefined;
+ this._models = [];
+ this._selectedModel = undefined;
+ }
+
+ async loadModels(): Promise {
+ if (!this.apiKey) {
+ throw new Error('Not authenticated');
+ }
+
+ try {
+ this._models = await this.client.fetchModels(this.apiKey);
+
+ // Check if currently selected model is still in the filtered list
+ if (this._selectedModel && !this._models.find(m => m.id === this._selectedModel!.id)) {
+ this._selectedModel = undefined;
+ }
+
+ // Set default model if none selected
+ if (!this._selectedModel && this._models.length > 0) {
+ // Try to find a good default model
+ const defaultModel = this._models.find(m =>
+ m.id.includes('gpt-4') ||
+ m.id.includes('claude') ||
+ m.id.includes('llama')
+ ) || this._models[0];
+
+ this._selectedModel = defaultModel;
+ }
+
+ return this._models;
+ } catch (error) {
+ if (this.isInferenceError(error) && error.type === 'auth') {
+ this._authError = error.message;
+ this.apiKey = undefined;
+ }
+ throw error;
+ }
+ }
+
+ selectModel(modelId: string): void {
+ const model = this._models.find(m => m.id === modelId);
+ if (!model) {
+ throw new Error(`Model ${modelId} not found`);
+ }
+ this._selectedModel = model;
+ }
+
+ async generateResponse(request: InferenceRequest): Promise {
+ if (!this.apiKey) {
+ throw new Error('Not authenticated');
+ }
+
+ if (!this._selectedModel) {
+ throw new Error('No model selected');
+ }
+
+ try {
+ return await this.client.generateResponse(
+ request,
+ this._selectedModel.id,
+ this.apiKey
+ );
+ } catch (error) {
+ if (this.isInferenceError(error) && error.type === 'auth') {
+ this._authError = error.message;
+ this.apiKey = undefined;
+ }
+ throw error;
+ }
+ }
+
+ getCapabilities(): ProviderCapabilities {
+ return {
+ authMethods: ['api_key'],
+ supportsModelListing: true,
+ requiresAuth: true,
+ };
+ }
+
+ private isInferenceError(error: any): error is InferenceError {
+ return error && typeof error === 'object' && 'type' in error && 'message' in error;
+ }
+}
\ No newline at end of file
diff --git a/src/providers/openrouter/client.ts b/src/providers/openrouter/client.ts
new file mode 100644
index 0000000..6de1fda
--- /dev/null
+++ b/src/providers/openrouter/client.ts
@@ -0,0 +1,295 @@
+// Shared OpenRouter HTTP client and utilities
+
+import type {
+ InferenceRequest,
+ InferenceResponse,
+ InferenceError,
+ ChatMessage,
+ ToolCall,
+ Model,
+ TokenUsage,
+} from '@/types/inference';
+import type {
+ OpenRouterBaseConfig,
+ OpenRouterChatRequest,
+ OpenRouterChatResponse,
+ OpenRouterModelsResponse,
+ OpenRouterModel,
+ OpenRouterMessage,
+ OpenRouterToolCall,
+ OpenRouterErrorResponse,
+} from './types';
+
+export class OpenRouterClient {
+ private baseUrl: string;
+ private httpReferrer?: string;
+ private appName?: string;
+
+ constructor(config: OpenRouterBaseConfig) {
+ this.baseUrl = config.baseUrl || 'https://openrouter.ai/api/v1';
+ this.httpReferrer = config.httpReferrer;
+ this.appName = config.appName;
+ }
+
+ async makeRequest(
+ endpoint: string,
+ options: RequestInit,
+ authToken: string
+ ): Promise {
+ const url = `${this.baseUrl}${endpoint}`;
+
+ const headers: HeadersInit = {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${authToken}`,
+ ...options.headers,
+ };
+
+ if (this.httpReferrer) {
+ headers['HTTP-Referer'] = this.httpReferrer;
+ }
+
+ if (this.appName) {
+ headers['X-Title'] = this.appName;
+ }
+
+ try {
+ const response = await fetch(url, {
+ ...options,
+ headers,
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json() as OpenRouterErrorResponse;
+ throw this.createInferenceError(response.status, errorData);
+ }
+
+ return await response.json() as T;
+ } catch (error) {
+ if (error instanceof Error && error.name === 'TypeError') {
+ // Network error
+ throw {
+ type: 'network',
+ message: 'Network request failed',
+ details: error,
+ retryable: true,
+ } as InferenceError;
+ }
+ throw error;
+ }
+ }
+
+ async fetchModels(authToken: string): Promise {
+ const response = await this.makeRequest(
+ '/models',
+ { method: 'GET' },
+ authToken
+ );
+
+ // Only return models that support tools
+ return response.data
+ .filter(model => model.supported_parameters?.includes('tools'))
+ .map(this.parseModel);
+ }
+
+ async generateResponse(
+ request: InferenceRequest,
+ model: string,
+ authToken: string
+ ): Promise {
+ const openRouterRequest: OpenRouterChatRequest = {
+ model,
+ messages: request.messages.map(this.formatMessage),
+ max_tokens: request.maxTokens,
+ temperature: request.temperature,
+ stop: request.stopSequences,
+ };
+
+ // Add tools if provided
+ if (request.tools && request.tools.length > 0) {
+ openRouterRequest.tools = request.tools;
+ openRouterRequest.tool_choice = 'auto';
+ }
+
+ const response = await this.makeRequest(
+ '/chat/completions',
+ {
+ method: 'POST',
+ body: JSON.stringify(openRouterRequest),
+ },
+ authToken
+ );
+
+ return this.parseResponse(response);
+ }
+
+ private parseModel(openRouterModel: OpenRouterModel): Model {
+ const inputCost = parseFloat(openRouterModel.pricing.prompt);
+ const outputCost = parseFloat(openRouterModel.pricing.completion);
+
+ // Check if model supports tools by looking at supported_parameters
+ const supportsTools = openRouterModel.supported_parameters?.includes('tools') || false;
+
+ return {
+ id: openRouterModel.id,
+ name: openRouterModel.name,
+ description: openRouterModel.description,
+ contextLength: openRouterModel.context_length,
+ inputCost: isNaN(inputCost) ? undefined : inputCost * 1000000, // Convert to per-token
+ outputCost: isNaN(outputCost) ? undefined : outputCost * 1000000,
+ provider: 'openrouter',
+ capabilities: {
+ supportsVision: openRouterModel.architecture.modality.includes('vision'),
+ maxTokens: openRouterModel.top_provider.max_completion_tokens || 4096,
+ },
+ };
+ }
+
+ private formatMessage(message: ChatMessage): OpenRouterMessage {
+ const openRouterMessage: OpenRouterMessage = {
+ role: message.role,
+ content: '',
+ };
+
+ // Handle content
+ if (typeof message.content === 'string') {
+ openRouterMessage.content = message.content;
+ } else {
+ // Handle content blocks
+ openRouterMessage.content = message.content.map(block => {
+ if (block.type === 'text') {
+ return {
+ type: 'text',
+ text: block.text || '',
+ };
+ } else {
+ return {
+ type: 'image_url',
+ image_url: {
+ url: block.imageUrl || '',
+ },
+ };
+ }
+ });
+ }
+
+ // Handle tool calls
+ if (message.toolCalls) {
+ openRouterMessage.tool_calls = message.toolCalls.map(toolCall => ({
+ id: toolCall.id,
+ type: 'function',
+ function: {
+ name: toolCall.function.name,
+ arguments: JSON.stringify(toolCall.function.arguments),
+ },
+ }));
+ }
+
+ // Handle tool call ID for tool response messages
+ if (message.toolCallId) {
+ openRouterMessage.tool_call_id = message.toolCallId;
+ }
+
+ return openRouterMessage;
+ }
+
+ private parseResponse(response: OpenRouterChatResponse): InferenceResponse {
+ const choice = response.choices[0];
+ const message = choice.message;
+
+ const chatMessage: ChatMessage = {
+ role: 'assistant',
+ content: message.content || '',
+ };
+
+ // Parse tool calls if present
+ if (message.tool_calls) {
+ chatMessage.toolCalls = message.tool_calls.map(this.parseToolCall);
+ }
+
+ const usage: TokenUsage = {
+ promptTokens: response.usage.prompt_tokens,
+ completionTokens: response.usage.completion_tokens,
+ totalTokens: response.usage.total_tokens,
+ };
+
+ let stopReason: InferenceResponse['stopReason'];
+ switch (choice.finish_reason) {
+ case 'stop':
+ stopReason = 'stop';
+ break;
+ case 'length':
+ stopReason = 'max_tokens';
+ break;
+ case 'tool_calls':
+ stopReason = 'tool_calls';
+ break;
+ default:
+ stopReason = 'error';
+ }
+
+ return {
+ message: chatMessage,
+ usage,
+ stopReason,
+ };
+ }
+
+ private parseToolCall(openRouterToolCall: OpenRouterToolCall): ToolCall {
+ let parsedArguments: Record;
+
+ try {
+ parsedArguments = JSON.parse(openRouterToolCall.function.arguments);
+ } catch (error) {
+ // If JSON parsing fails, create an error object
+ parsedArguments = {
+ _parseError: 'Invalid JSON in tool call arguments',
+ _rawArguments: openRouterToolCall.function.arguments,
+ };
+ }
+
+ return {
+ id: openRouterToolCall.id,
+ type: 'function',
+ function: {
+ name: openRouterToolCall.function.name,
+ arguments: parsedArguments,
+ },
+ };
+ }
+
+ private createInferenceError(status: number, errorData: OpenRouterErrorResponse): InferenceError {
+ const error = errorData.error;
+
+ let type: InferenceError['type'];
+ let retryable = false;
+
+ switch (status) {
+ case 401:
+ type = 'auth';
+ break;
+ case 429:
+ type = 'rate_limit';
+ retryable = true;
+ break;
+ case 400:
+ type = 'invalid_request';
+ break;
+ case 500:
+ case 502:
+ case 503:
+ case 504:
+ type = 'provider_error';
+ retryable = true;
+ break;
+ default:
+ type = 'provider_error';
+ }
+
+ return {
+ type,
+ message: error.message || `HTTP ${status} error`,
+ details: error,
+ retryable,
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/providers/openrouter/index.ts b/src/providers/openrouter/index.ts
new file mode 100644
index 0000000..1fe9326
--- /dev/null
+++ b/src/providers/openrouter/index.ts
@@ -0,0 +1,10 @@
+// OpenRouter provider exports
+
+export { OpenRouterApiProvider } from './api-provider';
+export { OpenRouterOAuthProvider } from './oauth-provider';
+export { OpenRouterClient } from './client';
+export type {
+ OpenRouterBaseConfig,
+ OpenRouterApiConfig,
+ OpenRouterOAuthConfig,
+} from './types';
\ No newline at end of file
diff --git a/src/providers/openrouter/oauth-provider.ts b/src/providers/openrouter/oauth-provider.ts
new file mode 100644
index 0000000..abbfb3f
--- /dev/null
+++ b/src/providers/openrouter/oauth-provider.ts
@@ -0,0 +1,356 @@
+// OpenRouter OAuth Provider
+
+import { InferenceProvider } from '@/types/inference';
+import type {
+ Model,
+ InferenceRequest,
+ InferenceResponse,
+ AuthConfig,
+ ProviderCapabilities,
+ InferenceError,
+} from '@/types/inference';
+import { OpenRouterClient } from './client';
+import type { OpenRouterOAuthConfig } from './types';
+
+interface OAuthState {
+ codeVerifier: string;
+ state: string;
+ expiresAt: number;
+}
+
+export class OpenRouterOAuthProvider extends InferenceProvider {
+ readonly name = 'OpenRouter (OAuth)';
+ readonly id = 'openrouter-oauth';
+
+ private client: OpenRouterClient;
+ private accessToken?: string;
+ private refreshToken?: string;
+ private _models: Model[] = [];
+ private _selectedModel?: Model;
+ private _authError?: string;
+ private oauthConfig: OpenRouterOAuthConfig;
+
+ constructor(config?: OpenRouterOAuthConfig) {
+ super();
+ this.oauthConfig = {
+ redirectUri: `${window.location.origin}/oauth/inference/callback`,
+ ...config,
+ };
+ this.client = new OpenRouterClient(config || {});
+ this.loadStoredTokens();
+ }
+
+ get isAuthenticated(): boolean {
+ return !!this.accessToken && !this._authError;
+ }
+
+ get authError(): string | undefined {
+ return this._authError;
+ }
+
+ get models(): Model[] {
+ return this._models;
+ }
+
+ get selectedModel(): Model | undefined {
+ return this._selectedModel;
+ }
+
+ async authenticate(config: AuthConfig): Promise {
+ if (config.type !== 'oauth') {
+ throw new Error('OpenRouterOAuthProvider requires OAuth authentication');
+ }
+
+ try {
+ await this.startOAuthFlow();
+ } catch (error) {
+ this._authError = error instanceof Error ? error.message : 'OAuth authentication failed';
+ throw error;
+ }
+ }
+
+ logout(): void {
+ this.accessToken = undefined;
+ this.refreshToken = undefined;
+ this._authError = undefined;
+ this._models = [];
+ this._selectedModel = undefined;
+ this.clearStoredTokens();
+ }
+
+ async loadModels(): Promise {
+ if (!this.accessToken) {
+ throw new Error('Not authenticated');
+ }
+
+ try {
+ this._models = await this.client.fetchModels(this.accessToken);
+
+ // Check if currently selected model is still in the filtered list
+ if (this._selectedModel && !this._models.find(m => m.id === this._selectedModel!.id)) {
+ this._selectedModel = undefined;
+ }
+
+ // Set default model if none selected
+ if (!this._selectedModel && this._models.length > 0) {
+ const defaultModel = this._models.find(m =>
+ m.id.includes('gpt-4') ||
+ m.id.includes('claude') ||
+ m.id.includes('llama')
+ ) || this._models[0];
+
+ this._selectedModel = defaultModel;
+ }
+
+ return this._models;
+ } catch (error) {
+ if (this.isInferenceError(error) && error.type === 'auth') {
+ // Try to refresh token
+ if (this.refreshToken) {
+ try {
+ await this.refreshAccessToken();
+ return await this.loadModels(); // Retry with new token
+ } catch (refreshError) {
+ this._authError = 'Token refresh failed';
+ this.logout();
+ }
+ } else {
+ this._authError = error.message;
+ this.accessToken = undefined;
+ }
+ }
+ throw error;
+ }
+ }
+
+ selectModel(modelId: string): void {
+ const model = this._models.find(m => m.id === modelId);
+ if (!model) {
+ throw new Error(`Model ${modelId} not found`);
+ }
+ this._selectedModel = model;
+ }
+
+ async generateResponse(request: InferenceRequest): Promise {
+ if (!this.accessToken) {
+ throw new Error('Not authenticated');
+ }
+
+ if (!this._selectedModel) {
+ throw new Error('No model selected');
+ }
+
+ try {
+ return await this.client.generateResponse(
+ request,
+ this._selectedModel.id,
+ this.accessToken
+ );
+ } catch (error) {
+ if (this.isInferenceError(error) && error.type === 'auth') {
+ // Try to refresh token
+ if (this.refreshToken) {
+ try {
+ await this.refreshAccessToken();
+ return await this.generateResponse(request); // Retry with new token
+ } catch (refreshError) {
+ this._authError = 'Token refresh failed';
+ this.logout();
+ }
+ } else {
+ this._authError = error.message;
+ this.accessToken = undefined;
+ }
+ }
+ throw error;
+ }
+ }
+
+ getCapabilities(): ProviderCapabilities {
+ return {
+ authMethods: ['oauth'],
+ supportsModelListing: true,
+ requiresAuth: true,
+ };
+ }
+
+ private async startOAuthFlow(): Promise {
+ // Generate PKCE parameters
+ const codeVerifier = this.generateCodeVerifier();
+ const codeChallenge = await this.generateCodeChallenge(codeVerifier);
+ const state = this.generateState();
+
+ // Store OAuth state
+ const oauthState: OAuthState = {
+ codeVerifier,
+ state,
+ expiresAt: Date.now() + (10 * 60 * 1000), // 10 minutes
+ };
+ localStorage.setItem('openrouter_oauth_state', JSON.stringify(oauthState));
+
+ // Build authorization URL
+ const authUrl = new URL('https://openrouter.ai/auth');
+ authUrl.searchParams.append('callback_url', this.oauthConfig.redirectUri || '');
+ authUrl.searchParams.append('code_challenge', codeChallenge);
+ authUrl.searchParams.append('code_challenge_method', 'S256');
+ authUrl.searchParams.append('state', state);
+
+ // Open popup for OAuth flow
+ const popup = window.open(
+ authUrl.toString(),
+ 'openrouter_oauth',
+ 'width=600,height=700,scrollbars=yes,resizable=yes'
+ );
+
+ if (!popup) {
+ throw new Error('Failed to open OAuth popup. Please allow popups for this site.');
+ }
+
+ // Wait for OAuth callback
+ return new Promise((resolve, reject) => {
+ let isResolved = false;
+
+ const handleMessage = (event: MessageEvent) => {
+ if (event.origin !== window.location.origin) return;
+
+ if (event.data.type === 'oauth_callback') {
+ if (isResolved) return; // Prevent double resolution
+ isResolved = true;
+
+ clearInterval(checkClosed);
+ window.removeEventListener('message', handleMessage);
+ popup.close();
+
+ if (event.data.error) {
+ reject(new Error(event.data.error));
+ } else {
+ this.handleOAuthCallback(event.data.code, event.data.state)
+ .then(resolve)
+ .catch(reject);
+ }
+ }
+ };
+
+ window.addEventListener('message', handleMessage);
+
+ // Check if popup was closed manually
+ const checkClosed = setInterval(() => {
+ if (popup.closed && !isResolved) {
+ isResolved = true;
+ clearInterval(checkClosed);
+ window.removeEventListener('message', handleMessage);
+ reject(new Error('OAuth flow was cancelled'));
+ }
+ }, 1000);
+ });
+ }
+
+ private async handleOAuthCallback(code: string, state: string): Promise {
+ // Verify state
+ const storedStateJson = localStorage.getItem('openrouter_oauth_state');
+ if (!storedStateJson) {
+ throw new Error('Invalid OAuth state');
+ }
+
+ const storedState: OAuthState = JSON.parse(storedStateJson);
+ if (storedState.state !== state || Date.now() > storedState.expiresAt) {
+ localStorage.removeItem('openrouter_oauth_state');
+ throw new Error('Invalid or expired OAuth state');
+ }
+
+ // Exchange code for tokens
+ try {
+ const response = await fetch('https://openrouter.ai/api/v1/auth/keys', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ code,
+ code_verifier: storedState.codeVerifier,
+ code_challenge_method: 'S256',
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Token exchange failed: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ this.accessToken = data.key; // OpenRouter returns 'key' field
+
+ // Store tokens
+ this.storeTokens();
+ localStorage.removeItem('openrouter_oauth_state');
+
+ // Load models to verify authentication
+ await this.loadModels();
+ } catch (error) {
+ localStorage.removeItem('openrouter_oauth_state');
+ throw error;
+ }
+ }
+
+ private async refreshAccessToken(): Promise {
+ // OpenRouter's OAuth implementation doesn't currently support refresh tokens
+ // If this changes in the future, implement refresh logic here
+ throw new Error('Token refresh not supported by OpenRouter');
+ }
+
+ private generateCodeVerifier(): string {
+ const array = new Uint8Array(32);
+ crypto.getRandomValues(array);
+ return btoa(String.fromCharCode.apply(null, Array.from(array)))
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=/g, '');
+ }
+
+ private async generateCodeChallenge(verifier: string): Promise {
+ const encoder = new TextEncoder();
+ const data = encoder.encode(verifier);
+ const digest = await crypto.subtle.digest('SHA-256', data);
+ return btoa(String.fromCharCode.apply(null, Array.from(new Uint8Array(digest))))
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=/g, '');
+ }
+
+ private generateState(): string {
+ const array = new Uint8Array(16);
+ crypto.getRandomValues(array);
+ return btoa(String.fromCharCode.apply(null, Array.from(array)));
+ }
+
+ private storeTokens(): void {
+ if (this.accessToken) {
+ localStorage.setItem('openrouter_access_token', this.accessToken);
+ }
+ if (this.refreshToken) {
+ localStorage.setItem('openrouter_refresh_token', this.refreshToken);
+ }
+ }
+
+ private loadStoredTokens(): void {
+ this.accessToken = localStorage.getItem('openrouter_access_token') || undefined;
+ this.refreshToken = localStorage.getItem('openrouter_refresh_token') || undefined;
+
+ // If we have tokens, try to load models to verify they're still valid
+ if (this.accessToken) {
+ this.loadModels().catch(() => {
+ // If loading fails, clear invalid tokens
+ this.logout();
+ });
+ }
+ }
+
+ private clearStoredTokens(): void {
+ localStorage.removeItem('openrouter_access_token');
+ localStorage.removeItem('openrouter_refresh_token');
+ localStorage.removeItem('openrouter_oauth_state');
+ }
+
+ private isInferenceError(error: any): error is InferenceError {
+ return error && typeof error === 'object' && 'type' in error && 'message' in error;
+ }
+}
\ No newline at end of file
diff --git a/src/providers/openrouter/types.ts b/src/providers/openrouter/types.ts
new file mode 100644
index 0000000..8fe7e22
--- /dev/null
+++ b/src/providers/openrouter/types.ts
@@ -0,0 +1,113 @@
+// OpenRouter-specific types and configurations
+
+export interface OpenRouterBaseConfig {
+ baseUrl?: string; // defaults to https://openrouter.ai/api/v1
+ defaultModel?: string;
+ httpReferrer?: string;
+ appName?: string;
+}
+
+export interface OpenRouterApiConfig extends OpenRouterBaseConfig {
+ apiKey: string;
+}
+
+export interface OpenRouterOAuthConfig extends OpenRouterBaseConfig {
+ clientId?: string; // for custom OAuth apps
+ redirectUri?: string;
+}
+
+// OpenRouter API response types
+export interface OpenRouterModel {
+ id: string;
+ name: string;
+ description?: string;
+ context_length: number;
+ pricing: {
+ prompt: string;
+ completion: string;
+ };
+ top_provider: {
+ max_completion_tokens?: number;
+ };
+ architecture: {
+ modality: string;
+ tokenizer: string;
+ instruct_type?: string;
+ };
+ supported_parameters?: string[];
+}
+
+export interface OpenRouterModelsResponse {
+ data: OpenRouterModel[];
+}
+
+export interface OpenRouterChatRequest {
+ model: string;
+ messages: OpenRouterMessage[];
+ tools?: OpenRouterTool[];
+ tool_choice?: string;
+ max_tokens?: number;
+ temperature?: number;
+ stop?: string[];
+ stream?: boolean;
+}
+
+export interface OpenRouterMessage {
+ role: 'user' | 'assistant' | 'system' | 'tool';
+ content: string | OpenRouterContentBlock[];
+ tool_calls?: OpenRouterToolCall[];
+ tool_call_id?: string;
+}
+
+export interface OpenRouterContentBlock {
+ type: 'text' | 'image_url';
+ text?: string;
+ image_url?: {
+ url: string;
+ };
+}
+
+export interface OpenRouterToolCall {
+ id: string;
+ type: 'function';
+ function: {
+ name: string;
+ arguments: string; // JSON string from API
+ };
+}
+
+export interface OpenRouterTool {
+ type: 'function';
+ function: {
+ name: string;
+ description?: string;
+ parameters: Record;
+ };
+}
+
+export interface OpenRouterChatResponse {
+ id: string;
+ choices: {
+ index: number;
+ message: {
+ role: 'assistant';
+ content: string | null;
+ tool_calls?: OpenRouterToolCall[];
+ };
+ finish_reason: 'stop' | 'length' | 'tool_calls' | 'content_filter';
+ }[];
+ usage: {
+ prompt_tokens: number;
+ completion_tokens: number;
+ total_tokens: number;
+ };
+ model: string;
+}
+
+export interface OpenRouterErrorResponse {
+ error: {
+ type: string;
+ message: string;
+ code?: string;
+ };
+}
\ No newline at end of file
diff --git a/src/types/inference.ts b/src/types/inference.ts
new file mode 100644
index 0000000..6f04183
--- /dev/null
+++ b/src/types/inference.ts
@@ -0,0 +1,109 @@
+// Core inference types and interfaces
+
+export interface Model {
+ id: string;
+ name: string;
+ description?: string;
+ contextLength: number;
+ inputCost?: number; // per token
+ outputCost?: number; // per token
+ provider: string;
+ capabilities: ModelCapabilities;
+}
+
+export interface ModelCapabilities {
+ supportsVision: boolean;
+ maxTokens: number;
+}
+
+export interface ChatMessage {
+ role: 'user' | 'assistant' | 'system' | 'tool';
+ content: string | ContentBlock[];
+ toolCalls?: ToolCall[];
+ toolCallId?: string; // for tool response messages
+}
+
+export interface ContentBlock {
+ type: 'text' | 'image';
+ text?: string;
+ imageUrl?: string;
+}
+
+export interface ToolCall {
+ id: string;
+ type: 'function';
+ function: {
+ name: string;
+ arguments: Record; // Parsed JSON object
+ };
+}
+
+export interface Tool {
+ type: 'function';
+ function: {
+ name: string;
+ description?: string;
+ parameters: Record; // JSON schema
+ };
+}
+
+export interface InferenceRequest {
+ messages: ChatMessage[];
+ tools?: Tool[]; // toolChoice defaults to 'auto' when tools provided
+ maxTokens?: number;
+ temperature?: number;
+ stopSequences?: string[];
+}
+
+export interface InferenceResponse {
+ message: ChatMessage;
+ usage: TokenUsage;
+ stopReason: 'stop' | 'max_tokens' | 'tool_calls' | 'error';
+ error?: string;
+}
+
+export interface TokenUsage {
+ promptTokens: number;
+ completionTokens: number;
+ totalTokens: number;
+ cost?: number;
+}
+
+export interface AuthConfig {
+ type: 'api_key' | 'oauth';
+ apiKey?: string;
+ oauthConfig?: {
+ clientId?: string;
+ redirectUri?: string;
+ scopes?: string[];
+ };
+}
+
+export interface ProviderCapabilities {
+ authMethods: ('api_key' | 'oauth')[];
+ supportsModelListing: boolean;
+ requiresAuth: boolean;
+}
+
+export interface InferenceError {
+ type: 'auth' | 'network' | 'rate_limit' | 'invalid_request' | 'provider_error';
+ message: string;
+ details?: any;
+ retryable: boolean;
+}
+
+export abstract class InferenceProvider {
+ abstract readonly name: string;
+ abstract readonly id: string;
+ abstract readonly isAuthenticated: boolean;
+ abstract readonly authError?: string;
+ abstract readonly models: Model[];
+ abstract readonly selectedModel?: Model;
+
+ abstract generateResponse(request: InferenceRequest): Promise;
+ abstract loadModels(): Promise;
+ abstract selectModel(modelId: string): void;
+ abstract authenticate(config: AuthConfig): Promise;
+ abstract logout(): void;
+ abstract getCapabilities(): ProviderCapabilities;
+}
\ No newline at end of file
diff --git a/src/utils/testTools.ts b/src/utils/testTools.ts
new file mode 100644
index 0000000..5257ace
--- /dev/null
+++ b/src/utils/testTools.ts
@@ -0,0 +1,108 @@
+// Test tools for validating tool calling functionality
+
+import type { Tool } from '@/types/inference';
+
+export const testTools: Tool[] = [
+ {
+ type: 'function',
+ function: {
+ name: 'get_weather',
+ description: 'Get the current weather for a location',
+ parameters: {
+ type: 'object',
+ properties: {
+ location: {
+ type: 'string',
+ description: 'The city and state, e.g. San Francisco, CA',
+ },
+ unit: {
+ type: 'string',
+ enum: ['celsius', 'fahrenheit'],
+ description: 'The temperature unit to use',
+ default: 'fahrenheit',
+ },
+ },
+ required: ['location'],
+ },
+ },
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'calculate_sum',
+ description: 'Calculate the sum of two numbers',
+ parameters: {
+ type: 'object',
+ properties: {
+ a: {
+ type: 'number',
+ description: 'First number',
+ },
+ b: {
+ type: 'number',
+ description: 'Second number',
+ },
+ },
+ required: ['a', 'b'],
+ },
+ },
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'get_current_time',
+ description: 'Get the current time in a specific timezone',
+ parameters: {
+ type: 'object',
+ properties: {
+ timezone: {
+ type: 'string',
+ description: 'The timezone (e.g., America/New_York, Europe/London)',
+ default: 'UTC',
+ },
+ },
+ required: [],
+ },
+ },
+ },
+];
+
+// Mock tool execution for testing
+export function executeTestTool(toolName: string, args: Record): string {
+ switch (toolName) {
+ case 'get_weather':
+ return JSON.stringify({
+ location: args.location,
+ temperature: Math.floor(Math.random() * 30) + 10,
+ unit: args.unit || 'fahrenheit',
+ condition: ['sunny', 'cloudy', 'rainy', 'snowy'][Math.floor(Math.random() * 4)],
+ humidity: Math.floor(Math.random() * 50) + 30,
+ });
+
+ case 'calculate_sum':
+ const sum = (args.a || 0) + (args.b || 0);
+ return JSON.stringify({
+ a: args.a,
+ b: args.b,
+ sum,
+ operation: `${args.a} + ${args.b} = ${sum}`,
+ });
+
+ case 'get_current_time':
+ const now = new Date();
+ const timezone = args.timezone || 'UTC';
+ return JSON.stringify({
+ timezone,
+ current_time: now.toISOString(),
+ unix_timestamp: Math.floor(now.getTime() / 1000),
+ formatted: now.toLocaleString('en-US', {
+ timeZone: timezone === 'UTC' ? 'UTC' : timezone
+ }),
+ });
+
+ default:
+ return JSON.stringify({
+ error: `Unknown tool: ${toolName}`,
+ });
+ }
+}
\ No newline at end of file
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..df645aa
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,30 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ primary: {
+ 50: '#f0f9ff',
+ 500: '#3b82f6',
+ 600: '#2563eb',
+ 700: '#1d4ed8',
+ },
+ secondary: {
+ 50: '#f8fafc',
+ 100: '#f1f5f9',
+ 200: '#e2e8f0',
+ 500: '#64748b',
+ 600: '#475569',
+ 700: '#334155',
+ 800: '#1e293b',
+ 900: '#0f172a',
+ }
+ },
+ },
+ },
+ plugins: [],
+}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..714eb81
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+
+ /* Path mapping */
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src", "**/*.ts", "**/*.tsx"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
\ No newline at end of file
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..099658c
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..2d42021
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,23 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import path from 'path'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+ define: {
+ global: 'globalThis',
+ },
+ server: {
+ port: 3000,
+ },
+ build: {
+ outDir: 'dist',
+ sourcemap: true,
+ },
+})
\ No newline at end of file
From 12cb59e76588e0489d1397af3dc1498e773dfd8c Mon Sep 17 00:00:00 2001
From: Jerome
Date: Tue, 10 Jun 2025 22:55:33 +0100
Subject: [PATCH 02/33] Implement MCP OAuth authentication system
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add MCPOAuthProvider implementing OAuthClientProvider interface
- Implement popup-based OAuth flow with message passing
- Add automatic connection retry after OAuth completion
- Support client registration persistence per server URL
- Add proper error suppression for expected OAuth UnauthorizedErrors
- Create comprehensive OAuth implementation documentation
- Clean up debug logging with emoji status indicators
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
docs/mcp_oauth_implementation.md | 135 +++++++
docs/mcp_provider_interface.md | 305 ++++++++++++++
src/App.tsx | 51 ++-
src/components/MCPTest.tsx | 430 ++++++++++++++++++++
src/components/OAuthCallback.tsx | 13 +-
src/contexts/MCPContext.tsx | 352 ++++++++++++++++
src/mcp/connection.ts | 674 +++++++++++++++++++++++++++++++
src/types/mcp.ts | 132 ++++++
8 files changed, 2076 insertions(+), 16 deletions(-)
create mode 100644 docs/mcp_oauth_implementation.md
create mode 100644 docs/mcp_provider_interface.md
create mode 100644 src/components/MCPTest.tsx
create mode 100644 src/contexts/MCPContext.tsx
create mode 100644 src/mcp/connection.ts
create mode 100644 src/types/mcp.ts
diff --git a/docs/mcp_oauth_implementation.md b/docs/mcp_oauth_implementation.md
new file mode 100644
index 0000000..5c72143
--- /dev/null
+++ b/docs/mcp_oauth_implementation.md
@@ -0,0 +1,135 @@
+# MCP OAuth Implementation Guide
+
+## Overview
+
+This document explains how OAuth authentication is implemented for MCP servers in our client, including lessons learned from early implementation attempts.
+
+## Final Working Implementation
+
+### Architecture
+
+The OAuth implementation uses the MCP TypeScript SDK's built-in auth system with a custom `OAuthClientProvider` that handles browser-based popup flows:
+
+```typescript
+class MCPOAuthProvider implements OAuthClientProvider {
+ // Implements all required OAuth provider methods
+ // Handles popup-based authorization flow
+ // Manages client registration and token storage
+}
+```
+
+### Key Components
+
+1. **MCPOAuthProvider**: Implements the SDK's `OAuthClientProvider` interface
+2. **Popup-based authorization**: Opens OAuth server in popup window
+3. **Message passing**: Popup communicates auth code back to parent window
+4. **Automatic retry**: Connection automatically retries after OAuth completion
+
+### OAuth Flow
+
+1. **Initial connection attempt**: Fails with `UnauthorizedError` (expected)
+2. **SDK triggers OAuth**: Calls `redirectToAuthorization()` which opens popup
+3. **User authorizes**: Completes OAuth flow in popup window
+4. **Authorization code received**: Popup sends auth code to parent via `postMessage`
+5. **Token exchange**: OAuth provider immediately calls SDK's `auth()` with the code
+6. **Tokens stored**: SDK calls `saveTokens()` with access/refresh tokens
+7. **Connection retry**: OAuth completion triggers automatic connection retry
+8. **Success**: Connection succeeds with valid tokens
+
+## What We Got Wrong Initially
+
+### Mistake 1: Manual OAuth Flow Management
+
+**Wrong approach**: We initially tried to manually manage the entire OAuth flow outside of the SDK:
+- Manually opening authorization URLs
+- Manually exchanging authorization codes for tokens
+- Trying to inject tokens into the transport after the fact
+
+**Why it was wrong**: The MCP SDK already has a complete OAuth implementation. Fighting against it created timing issues and complexity.
+
+**Correct approach**: Implement the `OAuthClientProvider` interface and let the SDK handle the OAuth flow orchestration.
+
+### Mistake 2: Waiting for Authorization in `redirectToAuthorization`
+
+**Wrong approach**: Making `redirectToAuthorization()` wait for the popup to complete:
+```typescript
+async redirectToAuthorization(authorizationUrl: URL): Promise {
+ const popup = window.open(authorizationUrl);
+ // Wait for popup to complete and return auth code
+ return new Promise((resolve, reject) => {
+ // Listen for popup messages...
+ });
+}
+```
+
+**Why it was wrong**: The SDK's `auth()` function expects `redirectToAuthorization()` to return immediately after opening the authorization URL. The authorization code processing happens in a separate call.
+
+**Correct approach**: Return immediately from `redirectToAuthorization()` and process the authorization code asynchronously when received from the popup.
+
+### Mistake 3: Not Understanding the Two-Step OAuth Flow
+
+**Wrong approach**: Thinking the SDK's `auth()` function is called once and handles everything.
+
+**Why it was wrong**: The OAuth flow actually involves multiple interactions:
+1. First call to `auth()`: Starts authorization flow, calls `redirectToAuthorization()`
+2. User completes authorization in popup
+3. Authorization code received via popup message
+4. Second call to `auth()` with the authorization code: Exchanges code for tokens
+
+**Correct approach**: Handle authorization code reception asynchronously and immediately call `auth()` again with the code to complete token exchange.
+
+### Mistake 4: Wrong Error Handling Strategy
+
+**Wrong approach**: Treating initial `UnauthorizedError` as a real failure and showing error alerts.
+
+**Why it was wrong**: For OAuth servers, the initial connection attempt is *expected* to fail with `UnauthorizedError`. This triggers the OAuth flow.
+
+**Correct approach**: Suppress `UnauthorizedError` for OAuth servers and let the OAuth flow handle authentication automatically.
+
+## Key Implementation Details
+
+### Client Registration Per Server
+
+Client information is stored per server URL (not per connection) to avoid unnecessary re-registrations:
+```typescript
+// Generate consistent key for server
+private getServerKey(): string {
+ const url = new URL(this.serverUrl);
+ return btoa(`${url.hostname}${url.pathname}`).replace(/[+/=]/g, '');
+}
+```
+
+### Predictable Redirect URI
+
+Uses a single redirect URI with state-based session identification:
+- Redirect URI: `${origin}/oauth/mcp/callback`
+- State parameter: `${connectionId}.${randomString}`
+
+This avoids the "unregistered redirect_uri" error that occurs with dynamic URIs.
+
+### Automatic Connection Retry
+
+After OAuth completion, the connection automatically retries:
+```typescript
+private async handleOAuthSuccess(): Promise {
+ // Reset connection state and retry with new tokens
+ this.connection.status = 'connecting';
+ await this.connect();
+}
+```
+
+## Best Practices Learned
+
+1. **Follow the SDK patterns**: Don't fight against the SDK's built-in OAuth system
+2. **Handle expected failures gracefully**: OAuth servers will return 401 on first connection
+3. **Use consistent redirect URIs**: Avoid dynamic URIs that require pre-registration
+4. **Store client info per server**: Minimize unnecessary client registrations
+5. **Process auth codes immediately**: Don't store them for later processing
+6. **Implement proper error suppression**: Distinguish between expected OAuth errors and real failures
+
+## Future Improvements
+
+- Add token refresh handling for long-lived connections
+- Implement proper error recovery for failed OAuth flows
+- Add support for different OAuth grant types (currently only authorization code)
+- Consider adding OAuth scope configuration options
\ No newline at end of file
diff --git a/docs/mcp_provider_interface.md b/docs/mcp_provider_interface.md
new file mode 100644
index 0000000..b98c9ac
--- /dev/null
+++ b/docs/mcp_provider_interface.md
@@ -0,0 +1,305 @@
+# MCP Provider Interface Design
+
+> **⚠️ DRAFT/TENTATIVE DESIGN**
+> This document outlines a proposed design for the MCP provider system. The design is subject to iteration and refinement during implementation.
+
+## Overview
+
+The MCP Provider system manages connections to multiple MCP servers, providing a unified interface for tool calling, resource access, and connection management. It follows similar architectural patterns to the InferenceProvider system.
+
+## Goals
+
+1. **Multi-Server Support**: Connect to multiple MCP servers simultaneously
+2. **Transport Abstraction**: Support SSE and Streamable HTTP transports with automatic fallback
+3. **Tool Aggregation**: Combine tools from all servers with conflict resolution
+4. **Connection Management**: Handle connection lifecycle, reconnection, and error recovery
+5. **OAuth Integration**: Support MCP server OAuth flows with proper callback routing
+6. **Debug Visibility**: Comprehensive debugging interface for development and troubleshooting
+
+## Core Interfaces
+
+### MCPConnection
+```typescript
+interface MCPConnection {
+ id: string; // Unique connection identifier
+ name: string; // User-provided server name
+ url: string; // Server URL
+ status: 'connecting' | 'connected' | 'failed' | 'disconnected';
+ client?: Client; // MCP SDK client instance
+ transport: 'sse' | 'streamable-http';
+ authType: 'none' | 'oauth';
+
+ // Available capabilities
+ tools: Tool[]; // Tools with name-prefixed identifiers
+ resources?: Resource[]; // Available resources
+ prompts?: Prompt[]; // Available prompts
+
+ // Connection metadata
+ error?: string; // Last error message
+ lastConnected?: Date; // Last successful connection
+ capabilities?: ServerCapabilities; // Server-advertised capabilities
+
+ // Debug information
+ messageTrace?: MCPMessage[]; // Recent message exchange history
+ connectionAttempts: number; // Number of reconnection attempts
+}
+```
+
+### MCPServerConfig
+```typescript
+interface MCPServerConfig {
+ name: string; // User-provided display name
+ url: string; // Server endpoint URL
+ transport?: 'sse' | 'streamable-http' | 'auto'; // Default: auto-detect
+ authType?: 'none' | 'oauth'; // Default: none
+ oauthConfig?: {
+ // OAuth configuration if needed
+ clientId?: string;
+ };
+ autoReconnect?: boolean; // Default: true
+ maxReconnectAttempts?: number; // Default: 5
+}
+```
+
+### MCPContextValue
+```typescript
+interface MCPContextValue {
+ // Connection state
+ connections: MCPConnection[];
+ isLoading: boolean;
+ error: string | null;
+
+ // Connection management
+ addMcpServer: (config: MCPServerConfig) => Promise; // Returns connection ID
+ removeMcpServer: (connectionId: string) => void;
+ reconnectServer: (connectionId: string) => Promise;
+
+ // Tool access (explicit server routing)
+ getAllTools: () => Tool[]; // All tools with prefixed names
+ getToolsForServer: (connectionId: string) => Tool[];
+ callTool: (connectionId: string, toolName: string, args: any) => Promise;
+
+ // Resource access
+ getAllResources: () => Resource[];
+ getResourcesForServer: (connectionId: string) => Resource[];
+
+ // Status and debugging
+ getConnectedServers: () => MCPConnection[];
+ getServerStatus: (connectionId: string) => MCPConnection['status'];
+ getConnectionById: (connectionId: string) => MCPConnection | undefined;
+ getMessageTrace: (connectionId: string) => MCPMessage[];
+}
+```
+
+## Tool Name Resolution
+
+### Prefixing Strategy
+Tools from each server are prefixed with the user-provided server name to avoid conflicts:
+
+```typescript
+// Original tool from weather server: "get_weather"
+// Prefixed tool: "weather_server.get_weather"
+
+// Original tool from calendar server: "get_events"
+// Prefixed tool: "calendar.get_events"
+```
+
+### Tool Discovery and Registration
+```typescript
+class MCPConnection {
+ private async discoverTools(): Promise {
+ const tools = await this.client.listTools();
+ return tools.map(tool => ({
+ ...tool,
+ function: {
+ ...tool.function,
+ name: `${this.name}.${tool.function.name}`, // Add prefix
+ description: `[${this.name}] ${tool.function.description}`, // Add server context
+ }
+ }));
+ }
+}
+```
+
+## Connection Management
+
+### Connection Lifecycle
+```typescript
+class MCPConnection {
+ async connect(): Promise {
+ this.status = 'connecting';
+
+ try {
+ // 1. Try StreamableHTTP first
+ if (this.transport === 'auto' || this.transport === 'streamable-http') {
+ await this.tryStreamableHttp();
+ }
+
+ // 2. Fallback to SSE if needed
+ if (this.status !== 'connected' && (this.transport === 'auto' || this.transport === 'sse')) {
+ await this.trySSE();
+ }
+
+ // 3. Initialize capabilities
+ await this.initializeCapabilities();
+
+ this.status = 'connected';
+ this.lastConnected = new Date();
+ this.connectionAttempts = 0;
+
+ } catch (error) {
+ this.status = 'failed';
+ this.error = error.message;
+ this.connectionAttempts++;
+
+ if (this.autoReconnect && this.connectionAttempts < this.maxReconnectAttempts) {
+ setTimeout(() => this.connect(), this.getBackoffDelay());
+ }
+ }
+ }
+}
+```
+
+### Auto-Reconnection Strategy
+- Exponential backoff: 1s, 2s, 4s, 8s, 16s
+- Maximum 5 attempts by default
+- Reset attempt counter on successful connection
+- User can manually retry after max attempts reached
+
+## React Integration
+
+### MCPProvider Context
+```typescript
+export function MCPProvider({ children }: { children: ReactNode }) {
+ const [connections, setConnections] = useState([]);
+ const [connectionStates, setConnectionStates] = useState>({});
+
+ // Load persisted connections from localStorage on mount
+ useEffect(() => {
+ loadPersistedConnections();
+ }, []);
+
+ // Auto-save connections to localStorage
+ useEffect(() => {
+ persistConnections(connections);
+ }, [connections]);
+
+ // ... implementation
+}
+```
+
+### Hook Usage
+```typescript
+const {
+ connections,
+ addMcpServer,
+ callTool,
+ getAllTools
+} = useMCP();
+
+// Add a server
+const connectionId = await addMcpServer({
+ name: 'Weather Service',
+ url: 'https://weather.mcp.example.com',
+ transport: 'auto'
+});
+
+// Call a tool
+const result = await callTool(connectionId, 'get_weather', {
+ location: 'San Francisco'
+});
+```
+
+## Authentication & OAuth
+
+### OAuth Flow for MCP Servers
+Following the established callback routing pattern:
+- OAuth callbacks: `/oauth/mcp/{connectionId}/callback`
+- Server-specific state management
+- Automatic token refresh handling
+
+```typescript
+interface MCPOAuthProvider {
+ authenticate(connectionId: string): Promise;
+ refreshToken(connectionId: string): Promise;
+ getAuthStatus(connectionId: string): 'authenticated' | 'pending' | 'failed';
+}
+```
+
+## Error Handling & Debugging
+
+### Message Tracing
+```typescript
+interface MCPMessage {
+ timestamp: Date;
+ direction: 'sent' | 'received';
+ type: 'request' | 'response' | 'notification';
+ method?: string;
+ id?: string | number;
+ content: any;
+ error?: any;
+}
+```
+
+### Connection Debugging
+- Real-time connection status monitoring
+- Message trace viewer with filtering
+- Transport fallback information
+- OAuth flow status tracking
+- Tool discovery and registration logs
+
+## Implementation Strategy
+
+### Phase 1: Core Connection Management ⏳
+1. Basic MCPConnection class with transport support
+2. MCPProvider React context with connection lifecycle
+3. Tool discovery and name prefixing
+4. Basic error handling and reconnection
+
+### Phase 2: Debug Interface ⏳
+1. Connection status dashboard
+2. Tool listing and testing interface
+3. Message trace viewer
+4. Manual connection controls
+
+### Phase 3: OAuth & Advanced Features
+1. OAuth integration for authenticated servers
+2. Resource and prompt support
+3. Enhanced debugging and monitoring
+4. Performance optimizations
+
+### Phase 4: Integration
+1. Integration with agent loop and inference provider
+2. Unified tool calling across test tools and MCP tools
+3. Advanced error recovery and resilience
+
+## Security Considerations
+
+1. **URL Validation**: Validate MCP server URLs to prevent malicious redirects
+2. **OAuth Security**: Proper PKCE implementation for OAuth flows
+3. **Message Validation**: Validate all MCP messages against protocol schema
+4. **Storage Security**: Secure storage of OAuth tokens and sensitive config
+5. **CORS Handling**: Proper CORS error detection and user guidance
+
+## Testing Strategy
+
+### Unit Tests
+- MCPConnection class functionality
+- Tool name prefixing and conflict resolution
+- Connection lifecycle and error handling
+- OAuth flow simulation
+
+### Integration Tests
+- Real MCP server connections (with test servers)
+- Transport fallback scenarios
+- Multi-server tool calling
+- OAuth authentication flows
+
+## Future Enhancements
+
+1. **Advanced Tool Routing**: Smart tool routing based on capability matching
+2. **Connection Pooling**: Efficient connection reuse and management
+3. **Caching**: Tool schema and response caching
+4. **Monitoring**: Connection health monitoring and alerts
+5. **Batch Operations**: Bulk tool calling and resource access
+6. **Custom Transports**: Support for WebSocket and custom transport protocols
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index d269034..1338fa6 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,30 +1,63 @@
-import React from 'react'
+import React, { useState } from 'react'
import { InferenceProvider } from '@/contexts/InferenceContext'
+import { MCPProvider } from '@/contexts/MCPContext'
import { InferenceTest } from '@/components/InferenceTest'
+import { MCPTest } from '@/components/MCPTest'
import { OAuthCallback } from '@/components/OAuthCallback'
function App() {
// Simple routing based on pathname
const pathname = window.location.pathname;
const isInferenceOAuthCallback = pathname === '/oauth/inference/callback';
- const isMcpOAuthCallback = pathname.startsWith('/oauth/mcp/');
+ const isMcpOAuthCallback = pathname === '/oauth/mcp/callback';
if (isInferenceOAuthCallback) {
return ;
}
if (isMcpOAuthCallback) {
- // Extract server identifier from path like /oauth/mcp/server123/callback
- const serverMatch = pathname.match(/^\/oauth\/mcp\/([^\/]+)\/callback$/);
- const serverId = serverMatch?.[1];
- return ;
+ return ;
}
+ const [activeTab, setActiveTab] = useState<'inference' | 'mcp'>('inference');
+
return (
-
-
-
+
+
+ {/* Tab Navigation */}
+
+
+
+ setActiveTab('inference')}
+ className={`py-4 px-1 border-b-2 font-medium text-sm ${
+ activeTab === 'inference'
+ ? 'border-blue-500 text-blue-600 dark:text-blue-400'
+ : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
+ }`}
+ >
+ Inference Provider Test
+
+ setActiveTab('mcp')}
+ className={`py-4 px-1 border-b-2 font-medium text-sm ${
+ activeTab === 'mcp'
+ ? 'border-blue-500 text-blue-600 dark:text-blue-400'
+ : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
+ }`}
+ >
+ MCP Provider Test
+
+
+
+
+
+ {/* Tab Content */}
+ {activeTab === 'inference' &&
}
+ {activeTab === 'mcp' &&
}
+
+
)
}
diff --git a/src/components/MCPTest.tsx b/src/components/MCPTest.tsx
new file mode 100644
index 0000000..2636cc4
--- /dev/null
+++ b/src/components/MCPTest.tsx
@@ -0,0 +1,430 @@
+// Test UI for MCP provider functionality
+
+import React, { useState, useCallback } from 'react';
+import { useMCP } from '@/contexts/MCPContext';
+import type { MCPServerConfig } from '@/types/mcp';
+
+export function MCPTest() {
+ const {
+ connections,
+ isLoading,
+ error,
+ addMcpServer,
+ removeMcpServer,
+ reconnectServer,
+ getAllTools,
+ getToolsForServer,
+ callTool,
+ getAllResources,
+ getConnectedServers,
+ } = useMCP();
+
+ const [newServerName, setNewServerName] = useState('');
+ const [newServerUrl, setNewServerUrl] = useState('');
+ const [selectedConnectionId, setSelectedConnectionId] = useState('');
+ const [selectedToolName, setSelectedToolName] = useState('');
+ const [toolArgs, setToolArgs] = useState('{}');
+ const [toolResult, setToolResult] = useState('');
+
+ const handleAddServer = useCallback(async () => {
+ if (!newServerName.trim() || !newServerUrl.trim()) {
+ alert('Please enter both server name and URL');
+ return;
+ }
+
+ const config: MCPServerConfig = {
+ name: newServerName.trim(),
+ url: newServerUrl.trim(),
+ transport: 'auto',
+ authType: 'none',
+ autoReconnect: false, // Disable auto-reconnect for now to prevent loops
+ };
+
+ try {
+ await addMcpServer(config);
+ setNewServerName('');
+ setNewServerUrl('');
+ } catch (error) {
+ console.error('Failed to add server:', error);
+ alert(`Failed to add server: ${error instanceof Error ? error.message : 'Unknown error'}`);
+ }
+ }, [newServerName, newServerUrl, addMcpServer]);
+
+ const handleAddServerWithOAuth = useCallback(async () => {
+ if (!newServerName.trim() || !newServerUrl.trim()) {
+ alert('Please enter both server name and URL');
+ return;
+ }
+
+ const config: MCPServerConfig = {
+ name: newServerName.trim(),
+ url: newServerUrl.trim(),
+ transport: 'auto',
+ authType: 'oauth',
+ autoReconnect: false,
+ };
+
+ try {
+ await addMcpServer(config);
+ setNewServerName('');
+ setNewServerUrl('');
+ } catch (error) {
+ console.error('Failed to add server with OAuth:', error);
+ alert(`Failed to add server with OAuth: ${error instanceof Error ? error.message : 'Unknown error'}`);
+ }
+ }, [newServerName, newServerUrl, addMcpServer]);
+
+ const handleRemoveServer = useCallback((connectionId: string) => {
+ if (confirm('Are you sure you want to remove this server?')) {
+ removeMcpServer(connectionId);
+ if (selectedConnectionId === connectionId) {
+ setSelectedConnectionId('');
+ }
+ }
+ }, [removeMcpServer, selectedConnectionId]);
+
+ const handleReconnectServer = useCallback(async (connectionId: string) => {
+ try {
+ await reconnectServer(connectionId);
+ } catch (error) {
+ console.error('Failed to reconnect:', error);
+ alert(`Failed to reconnect: ${error instanceof Error ? error.message : 'Unknown error'}`);
+ }
+ }, [reconnectServer]);
+
+ const handleCallTool = useCallback(async () => {
+ if (!selectedConnectionId || !selectedToolName) {
+ alert('Please select a connection and tool');
+ return;
+ }
+
+ try {
+ const args = JSON.parse(toolArgs);
+ const result = await callTool(selectedConnectionId, selectedToolName, args);
+ setToolResult(JSON.stringify(result, null, 2));
+ } catch (error) {
+ console.error('Tool call failed:', error);
+ const errorResult = {
+ error: error instanceof Error ? error.message : 'Unknown error',
+ details: error,
+ };
+ setToolResult(JSON.stringify(errorResult, null, 2));
+ }
+ }, [selectedConnectionId, selectedToolName, toolArgs, callTool]);
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'connected': return 'text-green-600 dark:text-green-400';
+ case 'connecting': return 'text-yellow-600 dark:text-yellow-400';
+ case 'failed': return 'text-red-600 dark:text-red-400';
+ case 'disconnected': return 'text-gray-600 dark:text-gray-400';
+ default: return 'text-gray-600 dark:text-gray-400';
+ }
+ };
+
+ const getStatusIcon = (status: string) => {
+ switch (status) {
+ case 'connected': return '✅';
+ case 'connecting': return '🔄';
+ case 'failed': return '❌';
+ case 'disconnected': return '⚫';
+ default: return '❓';
+ }
+ };
+
+ const selectedConnection = connections.find(conn => conn.id === selectedConnectionId);
+ const selectedTools = selectedConnection ? getToolsForServer(selectedConnectionId) : [];
+ const allTools = getAllTools();
+ const allResources = getAllResources();
+ const connectedServers = getConnectedServers();
+
+ return (
+
+
+
+ MCP Provider Test
+
+
+ {/* Add Server Section */}
+
+
+ Add MCP Server
+
+
+
+ setNewServerName(e.target.value)}
+ />
+ setNewServerUrl(e.target.value)}
+ />
+
+
+
+
+ {isLoading ? 'Adding...' : 'Add Server (No Auth)'}
+
+
+ {isLoading ? 'Adding...' : 'Add Server (OAuth)'}
+
+
+
+
+ {/* Server List */}
+
+
+ Connected Servers ({connections.length})
+
+
+ {connections.length === 0 ? (
+
No servers added yet.
+ ) : (
+
+ {connections.map((connection) => (
+
+
+
+
+
{getStatusIcon(connection.status)}
+
+
+ {connection.name}
+
+
+ {connection.url}
+
+
+
+
+
+
+ Status: {connection.status}
+
+
+ Transport: {connection.transport}
+
+
+ Tools: {connection.tools.length}
+
+
+ Resources: {connection.resources.length}
+
+ {connection.connectionAttempts > 0 && (
+
+ Attempts: {connection.connectionAttempts}
+
+ )}
+
+
+ {connection.error && (
+
+ Error: {connection.error}
+
+ )}
+
+ {connection.lastConnected && (
+
+ Last connected: {connection.lastConnected.toLocaleString()}
+
+ )}
+
+
+
+ {connection.status === 'failed' && (
+ handleReconnectServer(connection.id)}
+ className="px-3 py-1 bg-yellow-600 text-white rounded-md hover:bg-yellow-700 text-sm"
+ >
+ Retry
+
+ )}
+ handleRemoveServer(connection.id)}
+ className="px-3 py-1 bg-red-600 text-white rounded-md hover:bg-red-700 text-sm"
+ >
+ Remove
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Tool Testing */}
+ {connectedServers.length > 0 && (
+
+
+ Tool Testing
+
+
+
+ {/* Server Selection */}
+
+
+ Select Server:
+
+ {
+ setSelectedConnectionId(e.target.value);
+ setSelectedToolName('');
+ }}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
+ >
+ Select a server
+ {connectedServers.map((connection) => (
+
+ {connection.name} ({connection.tools.length} tools)
+
+ ))}
+
+
+
+ {/* Tool Selection */}
+
+
+ Select Tool:
+
+ setSelectedToolName(e.target.value)}
+ disabled={!selectedConnectionId}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 disabled:opacity-50"
+ >
+ Select a tool
+ {selectedTools.map((tool) => (
+
+ {tool.function.name}
+
+ ))}
+
+
+
+
+ {/* Tool Arguments */}
+
+
+ Tool Arguments (JSON):
+
+
+
+ {/* Call Tool Button */}
+
+ {isLoading ? 'Calling...' : 'Call Tool'}
+
+
+ {/* Tool Result */}
+ {toolResult && (
+
+
+ Tool Result:
+
+
+ {toolResult}
+
+
+ )}
+
+ )}
+
+ {/* Summary Stats */}
+
+
+
+ {connections.length}
+
+
+ Total Servers
+
+
+
+
+
+ {connectedServers.length}
+
+
+ Connected
+
+
+
+
+
+ {allTools.length}
+
+
+ Total Tools
+
+
+
+
+
+ {allResources.length}
+
+
+ Total Resources
+
+
+
+
+ {/* Error Display */}
+ {error && (
+
+ )}
+
+ {/* All Tools List */}
+ {allTools.length > 0 && (
+
+
+ All Available Tools ({allTools.length})
+
+
+ {allTools.map((tool, index) => (
+
+
+ {tool.function.name}
+
+ {tool.function.description && (
+
+ {tool.function.description}
+
+ )}
+
+ ))}
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/OAuthCallback.tsx b/src/components/OAuthCallback.tsx
index fd69cb8..b96f3b8 100644
--- a/src/components/OAuthCallback.tsx
+++ b/src/components/OAuthCallback.tsx
@@ -4,10 +4,9 @@ import React, { useEffect } from 'react';
interface OAuthCallbackProps {
type: 'inference' | 'mcp';
- serverId?: string; // Required when type is 'mcp'
}
-export function OAuthCallback({ type, serverId }: OAuthCallbackProps) {
+export function OAuthCallback({ type }: OAuthCallbackProps) {
useEffect(() => {
// Extract OAuth parameters from URL
const urlParams = new URLSearchParams(window.location.search);
@@ -16,7 +15,8 @@ export function OAuthCallback({ type, serverId }: OAuthCallbackProps) {
const error = urlParams.get('error');
const errorDescription = urlParams.get('error_description');
- // Send result to parent window
+ // For MCP OAuth, we just pass the callback data to the parent window
+ // The parent window (which has the MCPProvider context) will handle the actual callback processing
if (window.opener) {
const messageType = type === 'inference' ? 'oauth_callback' : 'mcp_oauth_callback';
@@ -24,14 +24,13 @@ export function OAuthCallback({ type, serverId }: OAuthCallbackProps) {
window.opener.postMessage({
type: messageType,
callbackType: type,
- serverId,
error: errorDescription || error,
+ state,
}, window.location.origin);
} else if (code && state) {
window.opener.postMessage({
type: messageType,
callbackType: type,
- serverId,
code,
state,
}, window.location.origin);
@@ -39,8 +38,8 @@ export function OAuthCallback({ type, serverId }: OAuthCallbackProps) {
window.opener.postMessage({
type: messageType,
callbackType: type,
- serverId,
error: 'Invalid OAuth callback - missing code or state',
+ state,
}, window.location.origin);
}
@@ -52,7 +51,7 @@ export function OAuthCallback({ type, serverId }: OAuthCallbackProps) {
// Fallback if not in a popup - redirect to main app
window.location.href = '/';
}
- }, []);
+ }, [type]);
return (
diff --git a/src/contexts/MCPContext.tsx b/src/contexts/MCPContext.tsx
new file mode 100644
index 0000000..7794688
--- /dev/null
+++ b/src/contexts/MCPContext.tsx
@@ -0,0 +1,352 @@
+// React context for MCP server connection management
+
+import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode, useRef } from 'react';
+import { v4 as uuidv4 } from 'uuid';
+
+import type {
+ MCPConnection,
+ MCPServerConfig,
+ MCPResource,
+ MCPContextValue,
+ MCPError,
+} from '@/types/mcp';
+import type { Tool } from '@/types/inference';
+import { MCPConnectionManager } from '@/mcp/connection';
+
+const MCPContext = createContext
(null);
+
+interface MCPProviderProps {
+ children: ReactNode;
+}
+
+export function MCPProvider({ children }: MCPProviderProps) {
+ const [connections, setConnections] = useState([]);
+ const [managers, setManagers] = useState>(new Map());
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const hasLoadedPersisted = useRef(false);
+
+ // Load persisted connections from localStorage on mount
+ useEffect(() => {
+ // Prevent loading if we've already loaded
+ if (hasLoadedPersisted.current) {
+ return;
+ }
+
+ hasLoadedPersisted.current = true;
+
+ const loadPersistedConnections = async () => {
+ try {
+ const persistedData = localStorage.getItem('mcp_connections');
+ if (persistedData) {
+ const persistedConfigs: MCPServerConfig[] = JSON.parse(persistedData);
+
+ // Restore connections directly without using addMcpServer to avoid loops
+ for (const config of persistedConfigs) {
+ try {
+ const connectionId = uuidv4();
+ const manager = new MCPConnectionManager(connectionId, config);
+
+ // Add to managers map
+ setManagers(prev => new Map(prev).set(connectionId, manager));
+
+ // Add initial connection state
+ setConnections(prev => [...prev, manager.getConnection()]);
+
+ // Don't auto-connect during restoration - let user manually connect
+ console.log(`Restored connection config for ${config.name}`);
+ } catch (error) {
+ console.warn(`Failed to restore connection to ${config.name}:`, error);
+ }
+ }
+ }
+ } catch (error) {
+ console.error('Failed to load persisted MCP connections:', error);
+ }
+ };
+
+ loadPersistedConnections();
+ }, []); // Empty dependency array - only run once on mount
+
+ const persistConnections = useCallback(() => {
+ try {
+ const configs = connections.map(conn => conn.config);
+ localStorage.setItem('mcp_connections', JSON.stringify(configs));
+ } catch (error) {
+ console.error('Failed to persist MCP connections:', error);
+ }
+ }, [connections]);
+
+ // Auto-save connections to localStorage when they change
+ useEffect(() => {
+ // Only save if we've finished initial loading
+ if (hasLoadedPersisted.current && connections.length > 0) {
+ persistConnections();
+ }
+ }, [persistConnections]);
+
+ const addMcpServer = useCallback(async (config: MCPServerConfig): Promise => {
+ const connectionId = uuidv4();
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ // Create connection manager
+ const manager = new MCPConnectionManager(connectionId, config);
+
+ // Set up callback for connection state updates
+ manager.setConnectionUpdateCallback(() => {
+ setConnections(prev =>
+ prev.map(conn =>
+ conn.id === connectionId ? manager.getConnection() : conn
+ )
+ );
+ });
+
+ // Add to managers map
+ setManagers(prev => new Map(prev).set(connectionId, manager));
+
+ // Add initial connection state
+ setConnections(prev => [...prev, manager.getConnection()]);
+
+ // Attempt to connect
+ try {
+ await manager.connect();
+
+ // Update connection state after successful connection
+ setConnections(prev =>
+ prev.map(conn =>
+ conn.id === connectionId ? manager.getConnection() : conn
+ )
+ );
+ } catch (error) {
+ // Update connection state with error
+ setConnections(prev =>
+ prev.map(conn =>
+ conn.id === connectionId ? managers.get(connectionId)?.getConnection() || conn : conn
+ )
+ );
+
+ // For OAuth servers, initial connection failure is expected
+ // The OAuth flow will handle the authentication and retry automatically
+ console.log('Caught error during connection:', {
+ authType: config.authType,
+ errorType: typeof error,
+ errorConstructor: error?.constructor?.name,
+ errorMessage: error?.message,
+ errorDetails: error?.details,
+ detailsConstructor: error?.details?.constructor?.name,
+ fullError: error
+ });
+
+ const isUnauthorizedError =
+ config.authType === 'oauth' && (
+ (error instanceof Error && error.message === 'Unauthorized') ||
+ (error instanceof Error && error.constructor.name === 'UnauthorizedError') ||
+ (error && typeof error === 'object' && error.message === 'Unauthorized' &&
+ error.details && error.details.constructor && error.details.constructor.name === 'UnauthorizedError')
+ );
+
+ if (isUnauthorizedError) {
+ console.log('Initial OAuth connection failed as expected, OAuth flow will handle authentication...');
+ } else {
+ console.log('Non-OAuth error, propagating:', error);
+ // For non-OAuth errors, propagate the error
+ const errorMessage = error instanceof Error ? error.message : 'Failed to add MCP server';
+ setError(errorMessage);
+ throw error;
+ }
+ }
+
+ return connectionId;
+ } finally {
+ setIsLoading(false);
+ }
+ }, [managers]);
+
+ const removeMcpServer = useCallback((connectionId: string) => {
+ const manager = managers.get(connectionId);
+ if (manager) {
+ // Disconnect the server
+ manager.disconnect();
+
+ // Remove from managers
+ setManagers(prev => {
+ const newMap = new Map(prev);
+ newMap.delete(connectionId);
+ return newMap;
+ });
+
+ // Remove from connections
+ setConnections(prev => prev.filter(conn => conn.id !== connectionId));
+ }
+ }, [managers]);
+
+ const reconnectServer = useCallback(async (connectionId: string): Promise => {
+ const manager = managers.get(connectionId);
+ if (!manager) {
+ throw new Error(`Connection ${connectionId} not found`);
+ }
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ await manager.reconnect();
+
+ // Update connection state
+ setConnections(prev =>
+ prev.map(conn =>
+ conn.id === connectionId ? manager.getConnection() : conn
+ )
+ );
+ } catch (error) {
+ // Update connection state with error
+ setConnections(prev =>
+ prev.map(conn =>
+ conn.id === connectionId ? manager.getConnection() : conn
+ )
+ );
+
+ const errorMessage = error instanceof Error ? error.message : 'Failed to reconnect server';
+ setError(errorMessage);
+ throw error;
+ } finally {
+ setIsLoading(false);
+ }
+ }, [managers]);
+
+ const getAllTools = useCallback((): Tool[] => {
+ return connections.flatMap(conn => conn.tools);
+ }, [connections]);
+
+ const getToolsForServer = useCallback((connectionId: string): Tool[] => {
+ const connection = connections.find(conn => conn.id === connectionId);
+ return connection?.tools || [];
+ }, [connections]);
+
+ const callTool = useCallback(async (connectionId: string, toolName: string, args: any): Promise => {
+ const manager = managers.get(connectionId);
+ if (!manager) {
+ throw new Error(`Connection ${connectionId} not found`);
+ }
+
+ try {
+ return await manager.callTool(toolName, args);
+ } catch (error) {
+ // Update connection state in case status changed
+ setConnections(prev =>
+ prev.map(conn =>
+ conn.id === connectionId ? manager.getConnection() : conn
+ )
+ );
+ throw error;
+ }
+ }, [managers]);
+
+ const getAllResources = useCallback((): MCPResource[] => {
+ return connections.flatMap(conn => conn.resources);
+ }, [connections]);
+
+ const getResourcesForServer = useCallback((connectionId: string): MCPResource[] => {
+ const connection = connections.find(conn => conn.id === connectionId);
+ return connection?.resources || [];
+ }, [connections]);
+
+ const getConnectedServers = useCallback((): MCPConnection[] => {
+ return connections.filter(conn => conn.status === 'connected');
+ }, [connections]);
+
+ const getServerStatus = useCallback((connectionId: string): MCPConnection['status'] => {
+ const connection = connections.find(conn => conn.id === connectionId);
+ return connection?.status || 'disconnected';
+ }, [connections]);
+
+ const getConnectionById = useCallback((connectionId: string): MCPConnection | undefined => {
+ return connections.find(conn => conn.id === connectionId);
+ }, [connections]);
+
+ const updateServerConfig = useCallback((connectionId: string, configUpdate: Partial) => {
+ setConnections(prev =>
+ prev.map(conn =>
+ conn.id === connectionId
+ ? { ...conn, config: { ...conn.config, ...configUpdate } }
+ : conn
+ )
+ );
+ }, []);
+
+ const handleOAuthCallback = useCallback(async (connectionId: string, authorizationCode: string): Promise => {
+ const manager = managers.get(connectionId);
+ if (!manager) {
+ throw new Error(`Connection ${connectionId} not found`);
+ }
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ // Type assertion to access the OAuth callback method
+ const connectionManager = manager as any;
+ if (typeof connectionManager.handleOAuthCallback === 'function') {
+ await connectionManager.handleOAuthCallback(authorizationCode);
+
+ // Update connection state
+ setConnections(prev =>
+ prev.map(conn =>
+ conn.id === connectionId ? manager.getConnection() : conn
+ )
+ );
+ } else {
+ throw new Error('OAuth callback not supported by this connection');
+ }
+ } catch (error) {
+ // Update connection state with error
+ setConnections(prev =>
+ prev.map(conn =>
+ conn.id === connectionId ? manager.getConnection() : conn
+ )
+ );
+
+ const errorMessage = error instanceof Error ? error.message : 'Failed to handle OAuth callback';
+ setError(errorMessage);
+ throw error;
+ } finally {
+ setIsLoading(false);
+ }
+ }, [managers]);
+
+ const contextValue: MCPContextValue = {
+ connections,
+ isLoading,
+ error,
+ addMcpServer,
+ removeMcpServer,
+ reconnectServer,
+ getAllTools,
+ getToolsForServer,
+ callTool,
+ getAllResources,
+ getResourcesForServer,
+ getConnectedServers,
+ getServerStatus,
+ getConnectionById,
+ updateServerConfig,
+ handleOAuthCallback,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useMCP(): MCPContextValue {
+ const context = useContext(MCPContext);
+ if (!context) {
+ throw new Error('useMCP must be used within an MCPProvider');
+ }
+ return context;
+}
\ No newline at end of file
diff --git a/src/mcp/connection.ts b/src/mcp/connection.ts
new file mode 100644
index 0000000..93b0f8a
--- /dev/null
+++ b/src/mcp/connection.ts
@@ -0,0 +1,674 @@
+// Individual MCP server connection management
+
+import { Client } from '@modelcontextprotocol/sdk/client/index.js';
+import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
+import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
+import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
+import {
+ auth,
+ discoverOAuthMetadata,
+ extractResourceMetadataUrl,
+ type OAuthClientProvider,
+ type OAuthClientMetadata,
+ type OAuthClientInformation,
+ type OAuthTokens,
+ type OAuthClientInformationFull
+} from '@modelcontextprotocol/sdk/client/auth.js';
+
+import type {
+ MCPConnection,
+ MCPServerConfig,
+ MCPResource,
+ MCPPrompt,
+ MCPConnectionManager,
+ MCPError,
+} from '@/types/mcp';
+import type { Tool } from '@/types/inference';
+
+interface MCPOAuthState {
+ codeVerifier: string;
+ state: string;
+ expiresAt: number;
+}
+
+class MCPOAuthProvider implements OAuthClientProvider {
+ private connectionId: string;
+ private serverName: string;
+ private serverUrl: string;
+ pendingAuthorizationCode?: string;
+ authError?: string;
+ private onOAuthComplete?: () => void;
+
+ constructor(connectionId: string, serverName: string, serverUrl: string, onOAuthComplete?: () => void) {
+ this.connectionId = connectionId;
+ this.serverName = serverName;
+ this.serverUrl = serverUrl;
+ this.onOAuthComplete = onOAuthComplete;
+ }
+
+ get redirectUrl(): string {
+ return `${window.location.origin}/oauth/mcp/callback`;
+ }
+
+ get clientMetadata(): OAuthClientMetadata {
+ return {
+ redirect_uris: [this.redirectUrl],
+ grant_types: ['authorization_code'],
+ response_types: ['code'],
+ client_name: `MCP Client - ${this.serverName}`,
+ token_endpoint_auth_method: 'none', // Public client
+ };
+ }
+
+ state(): string {
+ // Encode connection ID in the state parameter for callback identification
+ const randomPart = this.generateRandomString(8);
+ return `${this.connectionId}.${randomPart}`;
+ }
+
+ // Generate a consistent key for the server based on URL
+ getServerKey(): string {
+ try {
+ const url = new URL(this.serverUrl);
+ // Use hostname + pathname to create a unique but consistent key
+ // This allows the same server to reuse client registration even if added multiple times
+ return btoa(`${url.hostname}${url.pathname}`).replace(/[+/=]/g, '');
+ } catch {
+ // Fallback to connection ID if URL parsing fails
+ return this.connectionId;
+ }
+ }
+
+ clientInformation(): OAuthClientInformation | undefined {
+ const serverKey = this.getServerKey();
+ const stored = localStorage.getItem(`mcp_oauth_client_${serverKey}`);
+ return stored ? JSON.parse(stored) : undefined;
+ }
+
+ async saveClientInformation(clientInformation: OAuthClientInformationFull): Promise {
+ console.log('Registered OAuth client for MCP server');
+ const serverKey = this.getServerKey();
+ localStorage.setItem(`mcp_oauth_client_${serverKey}`, JSON.stringify(clientInformation));
+ }
+
+ tokens(): OAuthTokens | undefined {
+ const stored = localStorage.getItem(`mcp_oauth_tokens_${this.connectionId}`);
+ return stored ? JSON.parse(stored) : undefined;
+ }
+
+ async saveTokens(tokens: OAuthTokens): Promise {
+ console.log('OAuth tokens saved successfully');
+ localStorage.setItem(`mcp_oauth_tokens_${this.connectionId}`, JSON.stringify(tokens));
+ }
+
+ async redirectToAuthorization(authorizationUrl: URL): Promise {
+ console.log('🔐 Starting OAuth flow in popup window...');
+
+ // Open popup for OAuth flow
+ const popup = window.open(
+ authorizationUrl.toString(),
+ 'mcp_oauth_popup',
+ 'width=600,height=700,scrollbars=yes,resizable=yes'
+ );
+
+ if (!popup) {
+ throw new Error('Failed to open OAuth popup. Please allow popups for this site.');
+ }
+
+ // Set up message listener for the authorization code
+ const handleMessage = (event: MessageEvent) => {
+ if (event.origin !== window.location.origin) return;
+ if (event.data.type !== 'mcp_oauth_callback') return;
+
+ // Extract connection ID from state parameter
+ const state = event.data.state;
+ if (!state || !state.startsWith(this.connectionId + '.')) return;
+
+ window.removeEventListener('message', handleMessage);
+ popup.close();
+
+ if (event.data.error) {
+ console.error('❌ OAuth authorization failed:', event.data.error);
+ this.authError = event.data.error;
+ } else if (event.data.code) {
+ console.log('✅ OAuth authorization successful, exchanging code for tokens...');
+ this.processAuthorizationCode(event.data.code);
+ } else {
+ console.error('❌ OAuth callback missing authorization code');
+ this.authError = 'No authorization code received';
+ }
+ };
+
+ window.addEventListener('message', handleMessage);
+ }
+
+ async saveCodeVerifier(codeVerifier: string): Promise {
+ const oauthState: MCPOAuthState = {
+ codeVerifier,
+ state: this.generateRandomString(16),
+ expiresAt: Date.now() + (10 * 60 * 1000), // 10 minutes
+ };
+ localStorage.setItem(`mcp_oauth_state_${this.connectionId}`, JSON.stringify(oauthState));
+ }
+
+ async codeVerifier(): Promise {
+ const stored = localStorage.getItem(`mcp_oauth_state_${this.connectionId}`);
+ if (!stored) {
+ throw new Error('No OAuth state found');
+ }
+
+ const oauthState: MCPOAuthState = JSON.parse(stored);
+ if (Date.now() > oauthState.expiresAt) {
+ localStorage.removeItem(`mcp_oauth_state_${this.connectionId}`);
+ throw new Error('OAuth state expired');
+ }
+
+ return oauthState.codeVerifier;
+ }
+
+ private generateRandomString(length: number): string {
+ const array = new Uint8Array(length);
+ crypto.getRandomValues(array);
+ return btoa(String.fromCharCode.apply(null, Array.from(array)))
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=/g, '');
+ }
+
+ // Method to process authorization code immediately when received from popup
+ private async processAuthorizationCode(authorizationCode: string): Promise {
+ try {
+ // Call the SDK's auth function with the authorization code to exchange for tokens
+ const result = await auth(this, {
+ serverUrl: this.serverUrl,
+ authorizationCode,
+ });
+
+ if (result === 'AUTHORIZED') {
+ console.log('🎉 OAuth authentication completed! Connecting to MCP server...');
+ this.notifyOAuthComplete();
+ } else {
+ console.error('❌ OAuth token exchange failed');
+ this.authError = 'Authorization failed';
+ }
+ } catch (error) {
+ console.error('❌ OAuth token exchange error:', error instanceof Error ? error.message : error);
+ this.authError = error instanceof Error ? error.message : 'Authorization failed';
+ }
+ }
+
+ // Method to notify connection manager that OAuth is complete
+ private notifyOAuthComplete(): void {
+ if (this.onOAuthComplete) {
+ this.onOAuthComplete();
+ }
+ }
+
+ // Method to get and clear the pending authorization code
+ getPendingAuthorizationCode(): string | undefined {
+ const code = this.pendingAuthorizationCode;
+ this.pendingAuthorizationCode = undefined;
+ return code;
+ }
+}
+
+export class MCPConnectionManager implements MCPConnectionManager {
+ private connection: MCPConnection;
+ private client?: Client;
+ private transport?: Transport;
+ private reconnectTimeout?: NodeJS.Timeout;
+ private oauthProvider?: MCPOAuthProvider;
+ private onConnectionUpdate?: () => void;
+
+ constructor(id: string, config: MCPServerConfig) {
+ this.connection = {
+ id,
+ name: config.name,
+ url: config.url,
+ status: 'disconnected',
+ transport: 'streamable-http', // Will be determined during connection
+ authType: config.authType || 'none',
+ tools: [],
+ resources: [],
+ prompts: [],
+ connectionAttempts: 0,
+ config,
+ };
+
+ // Initialize OAuth provider if auth is required
+ if (this.connection.authType === 'oauth') {
+ this.oauthProvider = new MCPOAuthProvider(id, config.name, config.url, () => {
+ // Callback when OAuth completes successfully
+ this.handleOAuthSuccess();
+ });
+ }
+ }
+
+ getConnection(): MCPConnection {
+ return { ...this.connection };
+ }
+
+ // Set callback for connection state updates
+ setConnectionUpdateCallback(callback: () => void): void {
+ this.onConnectionUpdate = callback;
+ }
+
+ // Notify about connection state changes
+ private notifyConnectionUpdate(): void {
+ if (this.onConnectionUpdate) {
+ this.onConnectionUpdate();
+ }
+ }
+
+ async connect(): Promise {
+ this.connection.status = 'connecting';
+ this.connection.error = undefined;
+
+ try {
+ // Validate URL format
+ console.log('Validating URL:', this.connection.url);
+ try {
+ const url = new URL(this.connection.url);
+ console.log('URL parsed successfully:', {
+ protocol: url.protocol,
+ hostname: url.hostname,
+ pathname: url.pathname,
+ port: url.port
+ });
+ } catch (urlError) {
+ throw new Error(`Invalid URL format: ${this.connection.url}`);
+ }
+
+ // Clear any existing connections
+ await this.disconnect();
+
+ // Determine transport strategy
+ const transportPreference = this.connection.config.transport || 'auto';
+
+ if (transportPreference === 'auto' || transportPreference === 'streamable-http') {
+ try {
+ await this.tryStreamableHttp();
+ this.connection.transport = 'streamable-http';
+ } catch (error) {
+ if (transportPreference === 'streamable-http') {
+ throw error; // Don't fallback if explicitly requested
+ }
+ // Try SSE fallback
+ await this.trySSE();
+ this.connection.transport = 'sse';
+ }
+ } else if (transportPreference === 'sse') {
+ await this.trySSE();
+ this.connection.transport = 'sse';
+ }
+
+ // Initialize client capabilities
+ await this.initializeCapabilities();
+
+ this.connection.status = 'connected';
+ this.connection.lastConnected = new Date();
+ this.connection.connectionAttempts = 0;
+
+ } catch (error) {
+ this.connection.status = 'failed';
+ this.connection.error = error instanceof Error ? error.message : 'Unknown connection error';
+ this.connection.connectionAttempts++;
+
+ // Schedule auto-reconnect if enabled
+ if (this.connection.config.autoReconnect !== false &&
+ this.connection.connectionAttempts < (this.connection.config.maxReconnectAttempts || 5)) {
+ const delay = this.getBackoffDelay();
+ console.log(`Scheduling reconnect attempt ${this.connection.connectionAttempts} in ${delay}ms`);
+ this.reconnectTimeout = setTimeout(async () => {
+ try {
+ await this.reconnect();
+ } catch (error) {
+ console.error('Auto-reconnect failed:', error);
+ // Don't throw here to prevent uncaught promise rejection
+ }
+ }, delay);
+ }
+
+ throw this.createMCPError('connection', this.connection.error, error);
+ }
+ }
+
+ async disconnect(): Promise {
+ if (this.reconnectTimeout) {
+ clearTimeout(this.reconnectTimeout);
+ this.reconnectTimeout = undefined;
+ }
+
+ if (this.client) {
+ try {
+ await this.client.close();
+ } catch (error) {
+ // Ignore disconnect errors
+ }
+ this.client = undefined;
+ }
+
+ if (this.transport) {
+ try {
+ await this.transport.close();
+ } catch (error) {
+ // Ignore transport close errors
+ }
+ this.transport = undefined;
+ }
+
+ this.connection.status = 'disconnected';
+ this.connection.client = undefined;
+ }
+
+ // Method to handle OAuth callback with authorization code
+ async handleOAuthCallback(authorizationCode: string): Promise {
+ if (!this.oauthProvider) {
+ throw new Error('OAuth provider not initialized');
+ }
+
+ console.log('Processing OAuth callback with authorization code...');
+
+ try {
+ // If we have an active transport, use its finishAuth method
+ if (this.transport && typeof (this.transport as any).finishAuth === 'function') {
+ console.log('Calling transport.finishAuth...');
+ await (this.transport as any).finishAuth(authorizationCode);
+ console.log('OAuth authorization completed via transport');
+
+ // Now attempt to connect - the transport should be authenticated
+ await this.connect();
+ } else {
+ // If no transport yet, store the authorization code and try connecting
+ // The transport creation will handle the auth flow
+ this.oauthProvider.pendingAuthorizationCode = authorizationCode;
+ console.log('Stored authorization code, attempting connection...');
+ await this.connect();
+ }
+
+ console.log('OAuth callback processed successfully');
+ } catch (error) {
+ console.error('OAuth callback processing failed:', error);
+ throw new Error(`OAuth callback failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
+ }
+ }
+
+ // Method to clear stored OAuth data
+ clearOAuthData(): void {
+ if (this.connection.authType === 'oauth' && this.oauthProvider) {
+ // Clear connection-specific data
+ localStorage.removeItem(`mcp_oauth_tokens_${this.connection.id}`);
+ localStorage.removeItem(`mcp_oauth_state_${this.connection.id}`);
+
+ // Note: We intentionally don't clear client information here since it's
+ // shared across connections to the same server. Use clearSharedClientData()
+ // if you need to clear the client registration for this server.
+ }
+ }
+
+ // Method to clear shared client data for this server (affects all connections to the same server)
+ clearSharedClientData(): void {
+ if (this.connection.authType === 'oauth' && this.oauthProvider) {
+ const serverKey = this.oauthProvider.getServerKey();
+ localStorage.removeItem(`mcp_oauth_client_${serverKey}`);
+ }
+ }
+
+ async reconnect(): Promise {
+ await this.connect();
+ }
+
+ // Handle successful OAuth completion
+ private async handleOAuthSuccess(): Promise {
+ console.log('OAuth completed successfully, retrying connection...');
+
+ try {
+ // Reset connection state and retry
+ this.connection.status = 'connecting';
+ this.connection.error = undefined;
+ this.notifyConnectionUpdate();
+
+ // Attempt to connect now that we have valid tokens
+ await this.connect();
+
+ console.log('Post-OAuth connection successful!');
+ this.notifyConnectionUpdate();
+ } catch (error) {
+ console.error('Post-OAuth connection failed:', error);
+ this.connection.status = 'failed';
+ this.connection.error = error instanceof Error ? error.message : 'Post-OAuth connection failed';
+ this.notifyConnectionUpdate();
+ }
+ }
+
+ private async handleOAuthAuthentication(): Promise {
+ if (!this.oauthProvider) {
+ throw new Error('OAuth provider not initialized');
+ }
+
+ console.log('Starting OAuth authentication flow...');
+
+ try {
+ const result = await auth(this.oauthProvider, {
+ serverUrl: this.connection.url,
+ scope: this.connection.config.oauthConfig?.scope,
+ });
+
+ if (result === 'REDIRECT') {
+ // OAuth flow was initiated via popup, no further action needed here
+ console.log('OAuth flow initiated via popup');
+ } else if (result === 'AUTHORIZED') {
+ console.log('OAuth authentication successful');
+ }
+ } catch (error) {
+ console.error('OAuth authentication failed:', error);
+ throw new Error(`OAuth authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
+ }
+ }
+
+ private async tryStreamableHttp(): Promise {
+ try {
+ console.log('Attempting StreamableHTTP connection to:', this.connection.url);
+
+ // Create transport options with OAuth provider if configured
+ const transportOptions: any = {};
+
+ if (this.connection.authType === 'oauth' && this.oauthProvider) {
+ transportOptions.authProvider = this.oauthProvider;
+ }
+
+ const transport = new StreamableHTTPClientTransport(new URL(this.connection.url), transportOptions);
+ await this.initializeClient(transport);
+ console.log('StreamableHTTP connection successful');
+ } catch (error) {
+ console.error('StreamableHTTP connection failed:', error);
+ throw error;
+ }
+ }
+
+ private async trySSE(): Promise {
+ try {
+ console.log('Attempting SSE connection to:', this.connection.url);
+
+ // Create transport options with OAuth provider if configured
+ const transportOptions: any = {};
+
+ if (this.connection.authType === 'oauth' && this.oauthProvider) {
+ transportOptions.authProvider = this.oauthProvider;
+ }
+
+ const transport = new SSEClientTransport(new URL(this.connection.url), transportOptions);
+ console.log('SSE transport created, attempting client connection...');
+ await this.initializeClient(transport);
+ console.log('SSE connection successful');
+ } catch (error) {
+ console.error('SSE connection failed:', error);
+ console.error('Error details:', {
+ message: error instanceof Error ? error.message : 'Unknown error',
+ stack: error instanceof Error ? error.stack : undefined,
+ url: this.connection.url
+ });
+ throw error;
+ }
+ }
+
+ private async initializeClient(transport: Transport): Promise {
+ try {
+ console.log('Initializing MCP client...');
+ this.transport = transport;
+ this.client = new Client(
+ {
+ name: 'example-remote-client',
+ version: '1.0.0',
+ },
+ {
+ capabilities: {},
+ }
+ );
+
+ console.log('Connecting client to transport...');
+ console.log('Transport type:', transport.constructor.name);
+ console.log('Transport details:', transport);
+
+ await this.client.connect(transport);
+ this.connection.client = this.client;
+ console.log('Client connected successfully');
+ } catch (error) {
+ console.error('Client initialization failed:', error);
+ console.error('Error type:', error?.constructor?.name);
+ console.error('Error message:', error instanceof Error ? error.message : error);
+ console.error('Full error object:', error);
+ throw error;
+ }
+ }
+
+ private async initializeCapabilities(): Promise {
+ if (!this.client) {
+ throw new Error('Client not initialized');
+ }
+
+ try {
+ // Discover tools
+ this.connection.tools = await this.discoverTools();
+
+ // Discover resources (if supported)
+ try {
+ this.connection.resources = await this.discoverResources();
+ } catch (error) {
+ // Resources not supported by this server
+ this.connection.resources = [];
+ }
+
+ // Discover prompts (if supported)
+ try {
+ this.connection.prompts = await this.discoverPrompts();
+ } catch (error) {
+ // Prompts not supported by this server
+ this.connection.prompts = [];
+ }
+
+ } catch (error) {
+ throw this.createMCPError('protocol', 'Failed to initialize server capabilities', error);
+ }
+ }
+
+ async discoverTools(): Promise {
+ if (!this.client) {
+ throw new Error('Client not connected');
+ }
+
+ try {
+ const result = await this.client.listTools();
+
+ // Transform MCP tools to our Tool interface with name prefixing
+ return result.tools.map(tool => ({
+ type: 'function' as const,
+ function: {
+ name: `${this.connection.name}.${tool.name}`,
+ description: `[${this.connection.name}] ${tool.description || ''}`,
+ parameters: tool.inputSchema || {},
+ },
+ }));
+ } catch (error) {
+ throw this.createMCPError('protocol', 'Failed to discover tools', error);
+ }
+ }
+
+ async discoverResources(): Promise {
+ if (!this.client) {
+ throw new Error('Client not connected');
+ }
+
+ try {
+ const result = await this.client.listResources();
+
+ return result.resources.map(resource => ({
+ uri: resource.uri,
+ name: resource.name,
+ description: resource.description,
+ mimeType: resource.mimeType,
+ }));
+ } catch (error) {
+ throw this.createMCPError('protocol', 'Failed to discover resources', error);
+ }
+ }
+
+ async discoverPrompts(): Promise {
+ if (!this.client) {
+ throw new Error('Client not connected');
+ }
+
+ try {
+ const result = await this.client.listPrompts();
+
+ return result.prompts.map(prompt => ({
+ name: prompt.name,
+ description: prompt.description,
+ arguments: prompt.arguments,
+ }));
+ } catch (error) {
+ throw this.createMCPError('protocol', 'Failed to discover prompts', error);
+ }
+ }
+
+ async callTool(toolName: string, args: any): Promise {
+ if (!this.client) {
+ throw new Error('Client not connected');
+ }
+
+ // Remove the server prefix from the tool name
+ const unprefixedName = toolName.startsWith(`${this.connection.name}.`)
+ ? toolName.slice(this.connection.name.length + 1)
+ : toolName;
+
+ try {
+ const result = await this.client.callTool({
+ name: unprefixedName,
+ arguments: args,
+ });
+
+ return result;
+ } catch (error) {
+ throw this.createMCPError('tool_execution', `Failed to call tool ${unprefixedName}`, error);
+ }
+ }
+
+ getStatus(): MCPConnection['status'] {
+ return this.connection.status;
+ }
+
+ private getBackoffDelay(): number {
+ // Exponential backoff: 1s, 2s, 4s, 8s, 16s
+ return Math.min(1000 * Math.pow(2, this.connection.connectionAttempts - 1), 16000);
+ }
+
+ private createMCPError(type: MCPError['type'], message: string, details?: any): MCPError {
+ return {
+ type,
+ message,
+ details,
+ connectionId: this.connection.id,
+ retryable: type === 'connection' || type === 'transport',
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/types/mcp.ts b/src/types/mcp.ts
new file mode 100644
index 0000000..896b439
--- /dev/null
+++ b/src/types/mcp.ts
@@ -0,0 +1,132 @@
+// MCP provider types and interfaces
+
+import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
+import type { Tool } from './inference';
+
+export interface MCPServerConfig {
+ name: string; // User-provided display name
+ url: string; // Server endpoint URL
+ transport?: 'sse' | 'streamable-http' | 'auto'; // Default: auto-detect
+ authType?: 'none' | 'oauth'; // Default: none
+ oauthConfig?: {
+ clientId?: string;
+ authUrl?: string; // OAuth authorization endpoint
+ tokenUrl?: string; // Token exchange endpoint
+ scope?: string; // OAuth scope
+ redirectUri?: string; // Override default redirect URI
+ };
+ autoReconnect?: boolean; // Default: true
+ maxReconnectAttempts?: number; // Default: 5
+}
+
+export interface MCPMessage {
+ timestamp: Date;
+ direction: 'sent' | 'received';
+ type: 'request' | 'response' | 'notification';
+ method?: string;
+ id?: string | number;
+ content: any;
+ error?: any;
+}
+
+export interface MCPResource {
+ uri: string;
+ name: string;
+ description?: string;
+ mimeType?: string;
+}
+
+export interface MCPPrompt {
+ name: string;
+ description?: string;
+ arguments?: Array<{
+ name: string;
+ description?: string;
+ required?: boolean;
+ }>;
+}
+
+export interface MCPConnection {
+ id: string; // Unique connection identifier
+ name: string; // User-provided server name
+ url: string; // Server URL
+ status: 'connecting' | 'connected' | 'failed' | 'disconnected';
+ client?: Client; // MCP SDK client instance
+ transport: 'sse' | 'streamable-http';
+ authType: 'none' | 'oauth';
+
+ // Available capabilities
+ tools: Tool[]; // Tools with name-prefixed identifiers
+ resources: MCPResource[]; // Available resources
+ prompts: MCPPrompt[]; // Available prompts
+
+ // Connection metadata
+ error?: string; // Last error message
+ lastConnected?: Date; // Last successful connection
+ connectionAttempts: number; // Number of reconnection attempts
+
+ // Configuration
+ config: MCPServerConfig;
+}
+
+export interface MCPConnectionManager {
+ // Connection lifecycle
+ connect(): Promise;
+ disconnect(): void;
+ reconnect(): Promise;
+
+ // Capability discovery
+ discoverTools(): Promise;
+ discoverResources(): Promise;
+ discoverPrompts(): Promise;
+
+ // Tool execution
+ callTool(toolName: string, args: any): Promise;
+
+ // Status
+ getStatus(): MCPConnection['status'];
+ getConnection(): MCPConnection;
+
+ // Connection state updates
+ setConnectionUpdateCallback(callback: () => void): void;
+}
+
+export interface MCPError {
+ type: 'connection' | 'auth' | 'tool_execution' | 'protocol' | 'transport';
+ message: string;
+ details?: any;
+ connectionId?: string;
+ retryable: boolean;
+}
+
+export interface MCPContextValue {
+ // Connection state
+ connections: MCPConnection[];
+ isLoading: boolean;
+ error: string | null;
+
+ // Connection management
+ addMcpServer: (config: MCPServerConfig) => Promise; // Returns connection ID
+ removeMcpServer: (connectionId: string) => void;
+ reconnectServer: (connectionId: string) => Promise;
+
+ // Tool access (explicit server routing)
+ getAllTools: () => Tool[]; // All tools with prefixed names
+ getToolsForServer: (connectionId: string) => Tool[];
+ callTool: (connectionId: string, toolName: string, args: any) => Promise;
+
+ // Resource access
+ getAllResources: () => MCPResource[];
+ getResourcesForServer: (connectionId: string) => MCPResource[];
+
+ // Status and debugging
+ getConnectedServers: () => MCPConnection[];
+ getServerStatus: (connectionId: string) => MCPConnection['status'];
+ getConnectionById: (connectionId: string) => MCPConnection | undefined;
+
+ // Connection configuration
+ updateServerConfig: (connectionId: string, config: Partial) => void;
+
+ // OAuth handling
+ handleOAuthCallback: (connectionId: string, authorizationCode: string) => Promise;
+}
\ No newline at end of file
From 3a4a5cdc5201b7995f240f15cf69a25b363d1bbf Mon Sep 17 00:00:00 2001
From: Jerome
Date: Wed, 11 Jun 2025 18:16:27 +0100
Subject: [PATCH 03/33] Implement complete conversation system with agent loops
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add conversation management with persistence and multi-conversation support
- Create agent loop hook with test tools and MCP tool integration
- Implement full chat UI with message display, tool call visualization
- Add inline authentication flow for inference providers
- Support both API key and OAuth authentication methods
- Add conversation sidebar with conversation list and management
- Integrate MCP status display for debugging
- Create message input with proper disabled states and tooltips
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/App.tsx | 14 +-
src/components/ChatInterface.tsx | 145 +++++++++
src/components/ConversationApp.tsx | 45 +++
src/components/ConversationSidebar.tsx | 120 +++++++
src/components/InferenceLogin.tsx | 183 +++++++++++
src/components/MCPStatus.tsx | 150 +++++++++
src/components/MessageInput.tsx | 98 ++++++
src/components/MessageList.tsx | 200 ++++++++++++
src/contexts/ConversationContext.tsx | 291 +++++++++++++++++
src/contexts/InferenceContext.tsx | 9 +
src/hooks/useAgentLoop.ts | 419 +++++++++++++++++++++++++
src/types/conversation.ts | 126 ++++++++
12 files changed, 1799 insertions(+), 1 deletion(-)
create mode 100644 src/components/ChatInterface.tsx
create mode 100644 src/components/ConversationApp.tsx
create mode 100644 src/components/ConversationSidebar.tsx
create mode 100644 src/components/InferenceLogin.tsx
create mode 100644 src/components/MCPStatus.tsx
create mode 100644 src/components/MessageInput.tsx
create mode 100644 src/components/MessageList.tsx
create mode 100644 src/contexts/ConversationContext.tsx
create mode 100644 src/hooks/useAgentLoop.ts
create mode 100644 src/types/conversation.ts
diff --git a/src/App.tsx b/src/App.tsx
index 1338fa6..d1da0da 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -3,6 +3,7 @@ import { InferenceProvider } from '@/contexts/InferenceContext'
import { MCPProvider } from '@/contexts/MCPContext'
import { InferenceTest } from '@/components/InferenceTest'
import { MCPTest } from '@/components/MCPTest'
+import { ConversationApp } from '@/components/ConversationApp'
import { OAuthCallback } from '@/components/OAuthCallback'
function App() {
@@ -19,7 +20,7 @@ function App() {
return ;
}
- const [activeTab, setActiveTab] = useState<'inference' | 'mcp'>('inference');
+ const [activeTab, setActiveTab] = useState<'conversations' | 'inference' | 'mcp'>('conversations');
return (
@@ -29,6 +30,16 @@ function App() {
+ setActiveTab('conversations')}
+ className={`py-4 px-1 border-b-2 font-medium text-sm ${
+ activeTab === 'conversations'
+ ? 'border-blue-500 text-blue-600 dark:text-blue-400'
+ : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
+ }`}
+ >
+ Conversations
+
setActiveTab('inference')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
@@ -54,6 +65,7 @@ function App() {
{/* Tab Content */}
+ {activeTab === 'conversations' &&
}
{activeTab === 'inference' &&
}
{activeTab === 'mcp' &&
}
diff --git a/src/components/ChatInterface.tsx b/src/components/ChatInterface.tsx
new file mode 100644
index 0000000..e1bf3eb
--- /dev/null
+++ b/src/components/ChatInterface.tsx
@@ -0,0 +1,145 @@
+// Main chat interface with message display and input
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useConversation } from '@/contexts/ConversationContext';
+import { useInference } from '@/contexts/InferenceContext';
+import { MessageList } from './MessageList';
+import { MessageInput } from './MessageInput';
+import { InferenceLogin } from './InferenceLogin';
+
+export function ChatInterface() {
+ const {
+ activeConversationId,
+ getConversation,
+ sendMessage,
+ stopAgentLoop,
+ getAgentLoopState,
+ } = useConversation();
+
+ const { currentProvider } = useInference();
+ const [isLoading, setIsLoading] = useState(false);
+ const messagesEndRef = useRef
(null);
+
+ const activeConversation = activeConversationId ? getConversation(activeConversationId) : undefined;
+ const agentLoopState = activeConversationId ? getAgentLoopState(activeConversationId) : undefined;
+
+ // Auto-scroll to bottom when new messages arrive
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, [activeConversation?.messages]);
+
+ const handleSendMessage = async (content: string) => {
+ if (!activeConversationId || !currentProvider?.isAuthenticated) {
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ await sendMessage(activeConversationId, content);
+ } catch (error) {
+ console.error('Failed to send message:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleStopGeneration = () => {
+ if (activeConversationId) {
+ stopAgentLoop(activeConversationId);
+ }
+ };
+
+ // Show empty state if no conversation is selected
+ if (!activeConversationId || !activeConversation) {
+ return (
+
+
+
Welcome to MCP Chat
+
Start a new conversation to begin chatting with AI.
+
+ The AI has access to both test tools and any connected MCP servers.
+
+
+
+ );
+ }
+
+ // Show auth prompt if not authenticated
+ if (!currentProvider?.isAuthenticated) {
+ return (
+
+
+
+ );
+ }
+
+ const isGenerating = agentLoopState?.isRunning || isLoading;
+
+ return (
+
+ {/* Chat Header */}
+
+
+
+
+ {activeConversation.title}
+
+
+ Model: {currentProvider.selectedModel?.name || 'None selected'}
+
+
+
+ {isGenerating && (
+
+ Stop Generation
+
+ )}
+
+
+ {/* Status Indicator */}
+ {agentLoopState && (
+
+ {agentLoopState.isRunning && (
+
+
+
+ {agentLoopState.currentStep === 'inference' && 'Thinking...'}
+ {agentLoopState.currentStep === 'tool_execution' && 'Using tools...'}
+ {agentLoopState.currentStep === 'complete' && 'Complete'}
+
+
+ (Step {agentLoopState.iteration + 1}/{agentLoopState.maxIterations})
+
+
+ )}
+
+ )}
+
+
+ {/* Messages Area */}
+
+
+ {/* Input Area */}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/ConversationApp.tsx b/src/components/ConversationApp.tsx
new file mode 100644
index 0000000..86576aa
--- /dev/null
+++ b/src/components/ConversationApp.tsx
@@ -0,0 +1,45 @@
+// Main conversation application with sidebar and chat interface
+
+import React, { useState } from 'react';
+import { ConversationProvider } from '@/contexts/ConversationContext';
+import { ConversationSidebar } from './ConversationSidebar';
+import { ChatInterface } from './ChatInterface';
+import { MCPStatus } from './MCPStatus';
+
+export function ConversationApp() {
+ const [showMCPStatus, setShowMCPStatus] = useState(false);
+
+ return (
+
+
+ {/* Left Sidebar - Conversations */}
+
+
+
+
+ Conversations
+
+ setShowMCPStatus(!showMCPStatus)}
+ className="text-sm px-3 py-1 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"
+ >
+ {showMCPStatus ? 'Hide' : 'Show'} MCP
+
+
+
+
+ {showMCPStatus ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Main Chat Area */}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/ConversationSidebar.tsx b/src/components/ConversationSidebar.tsx
new file mode 100644
index 0000000..ca2490d
--- /dev/null
+++ b/src/components/ConversationSidebar.tsx
@@ -0,0 +1,120 @@
+// Conversation sidebar with conversation list and management
+
+import React from 'react';
+import { useConversation } from '@/contexts/ConversationContext';
+
+export function ConversationSidebar() {
+ const {
+ conversations,
+ activeConversationId,
+ createConversation,
+ deleteConversation,
+ setActiveConversation,
+ } = useConversation();
+
+ const handleNewConversation = () => {
+ createConversation();
+ };
+
+ const formatDate = (date: Date): string => {
+ const now = new Date();
+ const diff = now.getTime() - date.getTime();
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
+
+ if (days === 0) {
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+ } else if (days === 1) {
+ return 'Yesterday';
+ } else if (days < 7) {
+ return date.toLocaleDateString([], { weekday: 'short' });
+ } else {
+ return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
+ }
+ };
+
+ const getStatusIndicator = (status: string) => {
+ switch (status) {
+ case 'thinking':
+ return '💭';
+ case 'calling_tools':
+ return '🔧';
+ case 'error':
+ return '❌';
+ default:
+ return '';
+ }
+ };
+
+ return (
+
+ {/* New Conversation Button */}
+
+
+ + New Conversation
+
+
+
+ {/* Conversations List */}
+
+ {conversations.length === 0 ? (
+
+
No conversations yet.
+
Click "New Conversation" to get started.
+
+ ) : (
+
+ {conversations.map((conversation) => (
+
setActiveConversation(conversation.id)}
+ className={`p-3 rounded-lg cursor-pointer transition-colors group ${
+ activeConversationId === conversation.id
+ ? 'bg-blue-100 dark:bg-blue-900/50 border border-blue-200 dark:border-blue-800'
+ : 'hover:bg-gray-100 dark:hover:bg-gray-700'
+ }`}
+ >
+
+
+
+
+ {conversation.title}
+
+
+ {getStatusIndicator(conversation.status)}
+
+
+
+ {formatDate(conversation.updatedAt)}
+
+ {conversation.messages.length > 0 && (
+
+ {conversation.messages[conversation.messages.length - 1]?.content?.[0]?.type === 'text'
+ ? conversation.messages[conversation.messages.length - 1].content[0].text
+ : 'Tool interaction'
+ }
+
+ )}
+
+
+
{
+ e.stopPropagation();
+ deleteConversation(conversation.id);
+ }}
+ className="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 transition-all p-1"
+ title="Delete conversation"
+ >
+ ×
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/InferenceLogin.tsx b/src/components/InferenceLogin.tsx
new file mode 100644
index 0000000..dbb2163
--- /dev/null
+++ b/src/components/InferenceLogin.tsx
@@ -0,0 +1,183 @@
+// Inference provider login component for inline authentication
+
+import React, { useState, useMemo } from 'react';
+import { useInference } from '@/contexts/InferenceContext';
+import { OpenRouterApiProvider } from '@/providers/openrouter/api-provider';
+import { OpenRouterOAuthProvider } from '@/providers/openrouter/oauth-provider';
+
+export function InferenceLogin() {
+ const { provider: currentProvider, setProvider, models, refreshAuthState } = useInference();
+ const [selectedProviderId, setSelectedProviderId] = useState(currentProvider?.id || '');
+ const [apiKey, setApiKey] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ // Create available providers
+ const availableProviders = useMemo(() => [
+ new OpenRouterApiProvider(),
+ new OpenRouterOAuthProvider(),
+ ], []);
+
+ const selectedProvider = availableProviders.find(p => p.id === selectedProviderId);
+
+ const handleApiKeyLogin = async () => {
+ if (!selectedProvider || !apiKey.trim()) return;
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ await selectedProvider.authenticate({
+ type: 'api_key',
+ apiKey: apiKey.trim(),
+ });
+
+ // Load models after authentication
+ await selectedProvider.loadModels();
+
+ // Set this as the active provider
+ setProvider(selectedProvider);
+ refreshAuthState(); // Force re-render to show auth state
+ setApiKey(''); // Clear the API key input
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Authentication failed');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleOAuthLogin = async () => {
+ if (!selectedProvider) return;
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ await selectedProvider.authenticate({
+ type: 'oauth',
+ });
+
+ // Set this as the active provider
+ setProvider(selectedProvider);
+ refreshAuthState(); // Force re-render to show auth state
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'OAuth authentication failed');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const capabilities = selectedProvider?.getCapabilities();
+
+ return (
+
+
+
+ Connect to AI Provider
+
+
+ Choose a provider and authenticate to start chatting
+
+
+
+ {error && (
+
+ )}
+
+ {/* Provider Selection */}
+
+
+ AI Provider
+
+ setSelectedProviderId(e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+ >
+ Select a provider...
+ {availableProviders.map((provider) => (
+
+ {provider.name}
+
+ ))}
+
+
+
+ {selectedProvider && capabilities && (
+
+ {/* API Key Authentication */}
+ {capabilities.authMethods.includes('api_key') && (
+
+
+ API Key
+
+
+ setApiKey(e.target.value)}
+ placeholder="Enter your API key..."
+ className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && apiKey.trim()) {
+ handleApiKeyLogin();
+ }
+ }}
+ />
+
+ {isLoading ? 'Connecting...' : 'Connect'}
+
+
+
+ Your API key is stored locally and never sent to our servers
+
+
+ )}
+
+ {/* OAuth Authentication */}
+ {capabilities.authMethods.includes('oauth') && (
+
+ {capabilities.authMethods.includes('api_key') && (
+
+ )}
+
+
+ {isLoading ? 'Redirecting...' : `Sign in with ${selectedProvider.name} OAuth`}
+
+
+ )}
+
+ )}
+
+ {currentProvider?.isAuthenticated && models.length > 0 && (
+
+
+ ✓
+
+ Connected successfully! You can now start chatting.
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/MCPStatus.tsx b/src/components/MCPStatus.tsx
new file mode 100644
index 0000000..819d34b
--- /dev/null
+++ b/src/components/MCPStatus.tsx
@@ -0,0 +1,150 @@
+// MCP status display for the conversation sidebar
+
+import React from 'react';
+import { useMCP } from '@/contexts/MCPContext';
+
+export function MCPStatus() {
+ const { connections, getAllTools } = useMCP();
+
+ const connectedServers = connections.filter(conn => conn.status === 'connected');
+ const allTools = getAllTools();
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'connected':
+ return 'text-green-600 dark:text-green-400';
+ case 'connecting':
+ return 'text-yellow-600 dark:text-yellow-400';
+ case 'failed':
+ return 'text-red-600 dark:text-red-400';
+ default:
+ return 'text-gray-600 dark:text-gray-400';
+ }
+ };
+
+ const getStatusIcon = (status: string) => {
+ switch (status) {
+ case 'connected':
+ return '🟢';
+ case 'connecting':
+ return '🟡';
+ case 'failed':
+ return '🔴';
+ default:
+ return '⚪';
+ }
+ };
+
+ return (
+
+
+ {/* Summary */}
+
+
+ MCP Summary
+
+
+
+ {connectedServers.length} servers connected
+
+
+ {allTools.length} tools available
+
+
+
+
+ {/* Server List */}
+
+
+ Servers ({connections.length})
+
+
+ {connections.length === 0 ? (
+
+ No MCP servers configured. Go to the "MCP Provider Test" tab to add servers.
+
+ ) : (
+
+ {connections.map((connection) => (
+
+
+
+
+
+ {getStatusIcon(connection.status)}
+
+
+ {connection.name}
+
+
+
+ {connection.status}
+
+ {connection.error && (
+
+ {connection.error}
+
+ )}
+
+
+
+ {connection.tools.length > 0 && (
+
+
+ Tools ({connection.tools.length}):
+
+
+ {connection.tools.slice(0, 3).map((tool, index) => (
+
+ {tool.function.name.split('.').pop()}
+
+ ))}
+ {connection.tools.length > 3 && (
+
+ +{connection.tools.length - 3} more
+
+ )}
+
+
+ )}
+
+ ))}
+
+ )}
+
+
+ {/* Available Tools */}
+ {allTools.length > 0 && (
+
+
+ Available Tools ({allTools.length})
+
+
+ {allTools.map((tool, index) => (
+
+
+ {tool.function.name}
+
+ {tool.function.description && (
+
+ {tool.function.description}
+
+ )}
+
+ ))}
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/MessageInput.tsx b/src/components/MessageInput.tsx
new file mode 100644
index 0000000..ba31244
--- /dev/null
+++ b/src/components/MessageInput.tsx
@@ -0,0 +1,98 @@
+// Message input component with send functionality
+
+import React, { useState, useRef, useEffect } from 'react';
+
+interface MessageInputProps {
+ onSendMessage: (content: string) => void;
+ disabled?: boolean;
+ placeholder?: string;
+}
+
+export function MessageInput({
+ onSendMessage,
+ disabled = false,
+ placeholder = "Type your message..."
+}: MessageInputProps) {
+ const [message, setMessage] = useState('');
+ const textareaRef = useRef(null);
+
+ // Auto-resize textarea
+ useEffect(() => {
+ const textarea = textareaRef.current;
+ if (textarea) {
+ textarea.style.height = 'auto';
+ textarea.style.height = `${textarea.scrollHeight}px`;
+ }
+ }, [message]);
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const trimmedMessage = message.trim();
+ if (!trimmedMessage || disabled) return;
+
+ onSendMessage(trimmedMessage);
+ setMessage('');
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ // Send on Enter, new line on Shift+Enter
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ handleSubmit(e);
+ }
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/MessageList.tsx b/src/components/MessageList.tsx
new file mode 100644
index 0000000..bfc9200
--- /dev/null
+++ b/src/components/MessageList.tsx
@@ -0,0 +1,200 @@
+// Message list component to display conversation messages with tool calls
+
+import React from 'react';
+import type { ConversationMessage, ToolUseBlock, ToolResultBlock } from '@/types/conversation';
+
+interface MessageListProps {
+ messages: ConversationMessage[];
+ isLoading?: boolean;
+}
+
+export function MessageList({ messages, isLoading }: MessageListProps) {
+ const renderContentBlock = (block: any, messageId: string, blockIndex: number) => {
+ const key = `${messageId}-${blockIndex}`;
+
+ switch (block.type) {
+ case 'text':
+ return (
+
+ );
+
+ case 'tool_use':
+ return (
+
+
+ 🔧
+
+ Using tool: {block.name}
+
+
+
+
+ {JSON.stringify(block.input, null, 2)}
+
+
+
+ );
+
+ case 'tool_result':
+ const isError = block.is_error;
+ return (
+
+
+
+ {isError ? '❌' : '✅'}
+
+
+ Tool result
+
+
+
+ {typeof block.content === 'string' ? (
+
+ {block.content}
+
+ ) : (
+ block.content.map((contentItem: any, idx: number) => (
+
+ {contentItem.text && (
+
{contentItem.text}
+ )}
+ {contentItem.error && (
+
+ Error: {contentItem.error}
+
+ )}
+
+ ))
+ )}
+
+
+ );
+
+ default:
+ return (
+
+ Unknown content type: {block.type}
+
+ );
+ }
+ };
+
+ const formatTime = (date: Date): string => {
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+ };
+
+ const getRoleColor = (role: string): string => {
+ switch (role) {
+ case 'user':
+ return 'text-blue-600 dark:text-blue-400';
+ case 'assistant':
+ return 'text-green-600 dark:text-green-400';
+ case 'tool':
+ return 'text-purple-600 dark:text-purple-400';
+ default:
+ return 'text-gray-600 dark:text-gray-400';
+ }
+ };
+
+ const getRoleLabel = (role: string): string => {
+ switch (role) {
+ case 'user':
+ return 'You';
+ case 'assistant':
+ return 'Assistant';
+ case 'tool':
+ return 'Tool';
+ default:
+ return role;
+ }
+ };
+
+ if (messages.length === 0 && !isLoading) {
+ return (
+
+
+
Start a conversation
+
+ Ask a question or request help with something. The AI can use tools to help you.
+
+
+
+ );
+ }
+
+ return (
+
+ {messages.map((message) => (
+
+ {/* Avatar */}
+
+
+ {message.role === 'user' ? 'U' : message.role === 'assistant' ? 'A' : 'T'}
+
+
+
+ {/* Message Content */}
+
+
+
+ {getRoleLabel(message.role)}
+
+
+ {formatTime(message.timestamp)}
+
+
+
+ {/* Render content blocks */}
+
+ {message.content.map((block, index) =>
+ renderContentBlock(block, message.id, index)
+ )}
+
+
+
+ ))}
+
+ {/* Loading indicator */}
+ {isLoading && (
+
+
+
+
+
+ Assistant
+
+
+ thinking...
+
+
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/contexts/ConversationContext.tsx b/src/contexts/ConversationContext.tsx
new file mode 100644
index 0000000..d6ba3ec
--- /dev/null
+++ b/src/contexts/ConversationContext.tsx
@@ -0,0 +1,291 @@
+// React context for conversation management and agent loop orchestration
+
+import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode, useRef } from 'react';
+import { v4 as uuidv4 } from 'uuid';
+
+import type {
+ Conversation,
+ ConversationMessage,
+ ConversationContextValue,
+ AgentLoopState,
+ ConversationContentBlock,
+} from '@/types/conversation';
+import { useAgentLoop } from '@/hooks/useAgentLoop';
+
+const ConversationContext = createContext(null);
+
+interface ConversationProviderProps {
+ children: ReactNode;
+}
+
+export function ConversationProvider({ children }: ConversationProviderProps) {
+ const [conversations, setConversations] = useState([]);
+ const [activeConversationId, setActiveConversationId] = useState();
+ const [agentLoopStates, setAgentLoopStates] = useState>(new Map());
+
+ const hasLoadedPersisted = useRef(false);
+ const { executeAgentLoop, stopLoop, getLoopState } = useAgentLoop();
+
+ // Load persisted conversations from localStorage on mount
+ useEffect(() => {
+ if (hasLoadedPersisted.current) return;
+ hasLoadedPersisted.current = true;
+
+ try {
+ const persistedData = localStorage.getItem('conversations');
+ if (persistedData) {
+ const persistedConversations: Conversation[] = JSON.parse(persistedData);
+ // Convert date strings back to Date objects
+ const restored = persistedConversations.map(conv => ({
+ ...conv,
+ createdAt: new Date(conv.createdAt),
+ updatedAt: new Date(conv.updatedAt),
+ messages: conv.messages.map(msg => ({
+ ...msg,
+ timestamp: new Date(msg.timestamp),
+ })),
+ }));
+ setConversations(restored);
+
+ // Set active conversation to the most recent one
+ if (restored.length > 0) {
+ const mostRecent = restored.reduce((latest, conv) =>
+ conv.updatedAt > latest.updatedAt ? conv : latest
+ );
+ setActiveConversationId(mostRecent.id);
+ }
+ }
+ } catch (error) {
+ console.error('Failed to load persisted conversations:', error);
+ }
+ }, []);
+
+ // Persist conversations to localStorage when they change
+ useEffect(() => {
+ if (hasLoadedPersisted.current && conversations.length > 0) {
+ try {
+ localStorage.setItem('conversations', JSON.stringify(conversations));
+ } catch (error) {
+ console.error('Failed to persist conversations:', error);
+ }
+ }
+ }, [conversations]);
+
+ // Helper: Generate conversation title from first user message
+ const generateConversationTitle = useCallback((firstMessage: string): string => {
+ // Take first 50 characters and clean up
+ const title = firstMessage
+ .replace(/[^\w\s]/g, '')
+ .trim()
+ .substring(0, 50);
+
+ return title || 'New Conversation';
+ }, []);
+
+ const createConversation = useCallback((title?: string): string => {
+ const conversationId = uuidv4();
+ const now = new Date();
+
+ const newConversation: Conversation = {
+ id: conversationId,
+ title: title || 'New Conversation',
+ messages: [],
+ createdAt: now,
+ updatedAt: now,
+ status: 'idle',
+ };
+
+ setConversations(prev => [newConversation, ...prev]);
+ setActiveConversationId(conversationId);
+
+ return conversationId;
+ }, []);
+
+ const deleteConversation = useCallback((conversationId: string) => {
+ // Stop any running agent loop
+ stopLoop(conversationId);
+
+ // Remove from conversations
+ setConversations(prev => prev.filter(conv => conv.id !== conversationId));
+
+ // Remove agent loop state
+ setAgentLoopStates(prev => {
+ const newMap = new Map(prev);
+ newMap.delete(conversationId);
+ return newMap;
+ });
+
+ // If this was the active conversation, set to the next available one
+ if (activeConversationId === conversationId) {
+ setConversations(current => {
+ const remaining = current.filter(conv => conv.id !== conversationId);
+ setActiveConversationId(remaining.length > 0 ? remaining[0].id : undefined);
+ return remaining;
+ });
+ }
+ }, [activeConversationId, stopLoop]);
+
+ const setActiveConversation = useCallback((conversationId: string) => {
+ setActiveConversationId(conversationId);
+ }, []);
+
+ const updateConversationTitle = useCallback((conversationId: string, title: string) => {
+ setConversations(prev =>
+ prev.map(conv =>
+ conv.id === conversationId
+ ? { ...conv, title, updatedAt: new Date() }
+ : conv
+ )
+ );
+ }, []);
+
+ const getConversation = useCallback((conversationId: string): Conversation | undefined => {
+ return conversations.find(conv => conv.id === conversationId);
+ }, [conversations]);
+
+ const addUserMessage = useCallback((conversationId: string, content: string) => {
+ const userMessage: ConversationMessage = {
+ id: uuidv4(),
+ role: 'user',
+ content: [{ type: 'text', text: content }],
+ timestamp: new Date(),
+ };
+
+ setConversations(prev =>
+ prev.map(conv => {
+ if (conv.id === conversationId) {
+ // Auto-generate title from first user message
+ const title = conv.messages.length === 0
+ ? generateConversationTitle(content)
+ : conv.title;
+
+ return {
+ ...conv,
+ title,
+ messages: [...conv.messages, userMessage],
+ updatedAt: new Date(),
+ status: 'idle' as const,
+ };
+ }
+ return conv;
+ })
+ );
+ }, [generateConversationTitle]);
+
+ // Update conversation state from agent loop
+ const updateConversation = useCallback((updatedConversation: Conversation) => {
+ setConversations(prev =>
+ prev.map(conv =>
+ conv.id === updatedConversation.id ? updatedConversation : conv
+ )
+ );
+ }, []);
+
+ const sendMessage = useCallback(async (conversationId: string, content: string): Promise => {
+ // Add user message first
+ addUserMessage(conversationId, content);
+
+ // Get the updated conversation
+ const conversation = conversations.find(conv => conv.id === conversationId);
+ if (!conversation) {
+ throw new Error(`Conversation ${conversationId} not found`);
+ }
+
+ // Create updated conversation with the new user message
+ const userMessage: ConversationMessage = {
+ id: uuidv4(),
+ role: 'user',
+ content: [{ type: 'text', text: content }],
+ timestamp: new Date(),
+ };
+
+ const updatedConversation: Conversation = {
+ ...conversation,
+ messages: [...conversation.messages, userMessage],
+ updatedAt: new Date(),
+ status: 'thinking',
+ };
+
+ // Start agent loop
+ try {
+ await executeAgentLoop(updatedConversation, updateConversation);
+ } catch (error) {
+ // Update conversation with error state
+ const errorConversation: Conversation = {
+ ...updatedConversation,
+ status: 'error',
+ error: error instanceof Error ? error.message : 'Agent loop failed',
+ updatedAt: new Date(),
+ };
+ updateConversation(errorConversation);
+ throw error;
+ }
+ }, [conversations, addUserMessage, executeAgentLoop, updateConversation]);
+
+ const continueConversation = useCallback(async (conversationId: string): Promise => {
+ const conversation = conversations.find(conv => conv.id === conversationId);
+ if (!conversation) {
+ throw new Error(`Conversation ${conversationId} not found`);
+ }
+
+ try {
+ await executeAgentLoop(conversation, updateConversation);
+ } catch (error) {
+ const errorConversation: Conversation = {
+ ...conversation,
+ status: 'error',
+ error: error instanceof Error ? error.message : 'Agent loop failed',
+ updatedAt: new Date(),
+ };
+ updateConversation(errorConversation);
+ throw error;
+ }
+ }, [conversations, executeAgentLoop, updateConversation]);
+
+ const stopAgentLoop = useCallback((conversationId: string) => {
+ stopLoop(conversationId);
+
+ // Update conversation status
+ setConversations(prev =>
+ prev.map(conv =>
+ conv.id === conversationId
+ ? { ...conv, status: 'idle' as const, updatedAt: new Date() }
+ : conv
+ )
+ );
+ }, [stopLoop]);
+
+ const getAgentLoopState = useCallback((conversationId: string): AgentLoopState | undefined => {
+ return getLoopState(conversationId);
+ }, [getLoopState]);
+
+ const contextValue: ConversationContextValue = {
+ conversations,
+ activeConversationId,
+ agentLoopStates,
+ createConversation,
+ deleteConversation,
+ setActiveConversation,
+ updateConversationTitle,
+ getConversation,
+ addUserMessage,
+ sendMessage,
+ continueConversation,
+ stopAgentLoop,
+ getAgentLoopState,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useConversation(): ConversationContextValue {
+ const context = useContext(ConversationContext);
+ if (!context) {
+ throw new Error('useConversation must be used within a ConversationProvider');
+ }
+ return context;
+}
\ No newline at end of file
diff --git a/src/contexts/InferenceContext.tsx b/src/contexts/InferenceContext.tsx
index b82f9ec..c7246c8 100644
--- a/src/contexts/InferenceContext.tsx
+++ b/src/contexts/InferenceContext.tsx
@@ -17,6 +17,7 @@ interface InferenceContextValue {
// Provider actions
setProvider: (provider: InferenceProvider) => void;
clearProvider: () => void;
+ refreshAuthState: () => void; // Force refresh of auth state
// Inference actions
generateResponse: (request: InferenceRequest) => Promise;
@@ -40,11 +41,13 @@ export function InferenceProvider({ children }: InferenceProviderProps) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [selectedModelId, setSelectedModelId] = useState(undefined);
+ const [authStateVersion, setAuthStateVersion] = useState(0); // Force re-renders on auth changes
const setProvider = useCallback((newProvider: InferenceProvider) => {
setProviderState(newProvider);
setSelectedModelId(undefined);
setError(null);
+ setAuthStateVersion(prev => prev + 1); // Trigger re-render
}, []);
const clearProvider = useCallback(() => {
@@ -54,8 +57,13 @@ export function InferenceProvider({ children }: InferenceProviderProps) {
setProviderState(null);
setSelectedModelId(undefined);
setError(null);
+ setAuthStateVersion(prev => prev + 1);
}, [provider]);
+ const refreshAuthState = useCallback(() => {
+ setAuthStateVersion(prev => prev + 1);
+ }, []);
+
const generateResponse = useCallback(async (request: InferenceRequest): Promise => {
if (!provider) {
throw new Error('No inference provider configured');
@@ -120,6 +128,7 @@ export function InferenceProvider({ children }: InferenceProviderProps) {
error,
setProvider,
clearProvider,
+ refreshAuthState,
generateResponse,
selectModel,
loadModels,
diff --git a/src/hooks/useAgentLoop.ts b/src/hooks/useAgentLoop.ts
new file mode 100644
index 0000000..53eac38
--- /dev/null
+++ b/src/hooks/useAgentLoop.ts
@@ -0,0 +1,419 @@
+// Agent loop hook with MCP and test tool integration
+
+import { useCallback, useRef } from 'react';
+import { v4 as uuidv4 } from 'uuid';
+
+import type {
+ AgentLoopConfig,
+ AgentLoopState,
+ UseAgentLoopReturn,
+ ConversationMessage,
+ Conversation,
+ ToolUseBlock,
+ ToolResultBlock,
+ TestTool,
+} from '@/types/conversation';
+import type { ChatMessage, Tool, ToolCall, InferenceRequest } from '@/types/inference';
+import { useInference } from '@/contexts/InferenceContext';
+import { useMCP } from '@/contexts/MCPContext';
+
+// Test tools that work alongside MCP tools
+const testTools: TestTool[] = [
+ {
+ type: 'function',
+ function: {
+ name: 'get_weather',
+ description: 'Get current weather for a location',
+ parameters: {
+ type: 'object',
+ properties: {
+ location: {
+ type: 'string',
+ description: 'The city and state, e.g. San Francisco, CA',
+ },
+ },
+ required: ['location'],
+ },
+ },
+ execute: async (args) => {
+ // Mock weather data
+ const weather = {
+ location: args.location,
+ temperature: Math.floor(Math.random() * 30) + 10,
+ condition: ['sunny', 'cloudy', 'rainy', 'partly cloudy'][Math.floor(Math.random() * 4)],
+ humidity: Math.floor(Math.random() * 40) + 30,
+ };
+ // Simulate API delay
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ return weather;
+ },
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'calculate',
+ description: 'Perform basic arithmetic calculations',
+ parameters: {
+ type: 'object',
+ properties: {
+ expression: {
+ type: 'string',
+ description: 'Mathematical expression to evaluate (e.g., "2 + 2", "10 * 3")',
+ },
+ },
+ required: ['expression'],
+ },
+ },
+ execute: async (args) => {
+ try {
+ // Simple expression evaluator (basic safety check)
+ const expression = args.expression.replace(/[^0-9+\-*/().\s]/g, '');
+ if (expression !== args.expression) {
+ throw new Error('Invalid characters in expression');
+ }
+ const result = eval(expression);
+ return { expression: args.expression, result };
+ } catch (error) {
+ throw new Error(`Calculation error: ${error instanceof Error ? error.message : 'Unknown error'}`);
+ }
+ },
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'get_current_time',
+ description: 'Get the current date and time',
+ parameters: {
+ type: 'object',
+ properties: {
+ timezone: {
+ type: 'string',
+ description: 'Timezone (e.g., "UTC", "America/New_York")',
+ },
+ },
+ },
+ },
+ execute: async (args) => {
+ const options: Intl.DateTimeFormatOptions = {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+ second: 'numeric',
+ timeZoneName: 'short',
+ };
+
+ if (args.timezone) {
+ options.timeZone = args.timezone;
+ }
+
+ return {
+ timestamp: new Date().toISOString(),
+ formatted: new Date().toLocaleString('en-US', options),
+ timezone: args.timezone || 'local',
+ };
+ },
+ },
+];
+
+const DEFAULT_CONFIG: AgentLoopConfig = {
+ maxIterations: 10,
+ systemMessage: 'You are a helpful assistant with access to various tools. Use them when needed to answer questions accurately.',
+ temperature: 0.7,
+ stopOnError: false,
+};
+
+export function useAgentLoop(config: Partial = {}): UseAgentLoopReturn {
+ const finalConfig = { ...DEFAULT_CONFIG, ...config };
+ const { currentProvider } = useInference();
+ const { getAllTools, callTool: callMCPTool, connections } = useMCP();
+
+ // Track running loops
+ const loopStates = useRef>(new Map());
+
+ // Helper: Convert conversation messages to inference format
+ const toInferenceMessages = useCallback((messages: ConversationMessage[]): ChatMessage[] => {
+ return messages.map(msg => {
+ // Convert content blocks back to the inference format
+ if (msg.content.length === 1 && msg.content[0].type === 'text') {
+ // Simple text message
+ return {
+ role: msg.role,
+ content: msg.content[0].text,
+ };
+ }
+
+ // Complex message with tool calls/results - reconstruct the format
+ let content = '';
+ const toolCalls: ToolCall[] = [];
+ let toolCallId: string | undefined;
+
+ for (const block of msg.content) {
+ if (block.type === 'text') {
+ content += block.text;
+ } else if (block.type === 'tool_use') {
+ toolCalls.push({
+ id: block.id,
+ type: 'function',
+ function: {
+ name: block.name,
+ arguments: block.input,
+ },
+ });
+ } else if (block.type === 'tool_result') {
+ toolCallId = block.tool_use_id;
+ // For tool result messages, content comes from the tool result
+ if (typeof block.content === 'string') {
+ content = block.content;
+ } else if (Array.isArray(block.content)) {
+ content = block.content.map(c => c.text || c.error || '').join('');
+ }
+ }
+ }
+
+ return {
+ role: msg.role,
+ content: content || '',
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
+ toolCallId,
+ };
+ });
+ }, []);
+
+ // Helper: Create conversation message from inference response
+ const fromInferenceResponse = useCallback((response: ChatMessage): ConversationMessage => {
+ const content: any[] = [];
+
+ // Add text content if present
+ if (typeof response.content === 'string' && response.content) {
+ content.push({
+ type: 'text',
+ text: response.content,
+ });
+ }
+
+ // Add tool calls if present
+ if (response.toolCalls) {
+ for (const toolCall of response.toolCalls) {
+ content.push({
+ type: 'tool_use',
+ id: toolCall.id,
+ name: toolCall.function.name,
+ input: toolCall.function.arguments,
+ });
+ }
+ }
+
+ return {
+ id: uuidv4(),
+ role: response.role,
+ content,
+ timestamp: new Date(),
+ toolCalls: response.toolCalls,
+ toolCallId: response.toolCallId,
+ };
+ }, []);
+
+ // Helper: Execute a tool call
+ const executeTool = useCallback(async (toolCall: ToolCall): Promise<{ result: any; error?: string }> => {
+ try {
+ // Check if it's a test tool
+ const testTool = testTools.find(t => t.function.name === toolCall.function.name);
+ if (testTool) {
+ const result = await testTool.execute(toolCall.function.arguments);
+ return { result };
+ }
+
+ // Check if it's an MCP tool (prefixed with server name)
+ if (toolCall.function.name.includes('.')) {
+ // Extract server name from tool name (format: "server.tool_name")
+ const [serverName] = toolCall.function.name.split('.');
+
+ // Find the connection ID for this server name
+ const connection = connections.find(conn => conn.name === serverName);
+
+ if (connection) {
+ const result = await callMCPTool(connection.id, toolCall.function.name, toolCall.function.arguments);
+ return { result };
+ } else {
+ throw new Error(`MCP server "${serverName}" not found or not connected`);
+ }
+ }
+
+ throw new Error(`Unknown tool: ${toolCall.function.name}`);
+ } catch (error) {
+ return {
+ result: null,
+ error: error instanceof Error ? error.message : 'Tool execution failed',
+ };
+ }
+ }, [getAllTools, callMCPTool, connections]);
+
+ // Main agent loop execution - processes the conversation and generates responses with tool calls
+ const executeAgentLoop = useCallback(async (
+ conversation: Conversation,
+ onUpdate: (conversation: Conversation) => void
+ ) => {
+ if (!currentProvider?.isAuthenticated) {
+ throw new Error('No authenticated inference provider available');
+ }
+
+ const conversationId = conversation.id;
+
+ // Initialize loop state
+ const abortController = new AbortController();
+ const loopState: AgentLoopState = {
+ isRunning: true,
+ conversationId,
+ currentStep: 'inference',
+ iteration: 0,
+ maxIterations: finalConfig.maxIterations,
+ abortController,
+ };
+
+ loopStates.current.set(conversationId, loopState);
+
+ try {
+ let currentConversation = { ...conversation };
+
+ for (let iteration = 0; iteration < finalConfig.maxIterations; iteration++) {
+ if (abortController.signal.aborted) {
+ break;
+ }
+
+ loopState.iteration = iteration;
+ loopState.currentStep = 'inference';
+
+ // Convert to inference format and add system message if needed
+ const inferenceMessages = toInferenceMessages(currentConversation.messages);
+ if (finalConfig.systemMessage && (inferenceMessages.length === 0 || inferenceMessages[0].role !== 'system')) {
+ inferenceMessages.unshift({
+ role: 'system',
+ content: finalConfig.systemMessage,
+ });
+ }
+
+ // Get available tools (test tools + MCP tools)
+ const allTools: Tool[] = [
+ ...testTools.map(t => ({ type: t.type, function: t.function })),
+ ...getAllTools(),
+ ];
+
+ // Make inference request
+ const request: InferenceRequest = {
+ messages: inferenceMessages,
+ tools: allTools.length > 0 ? allTools : undefined,
+ temperature: finalConfig.temperature,
+ };
+
+ const response = await currentProvider.generateResponse(request);
+
+ if (abortController.signal.aborted) {
+ break;
+ }
+
+ // Add assistant response to conversation
+ const assistantMessage = fromInferenceResponse(response.message);
+
+ currentConversation = {
+ ...currentConversation,
+ messages: [...currentConversation.messages, assistantMessage],
+ updatedAt: new Date(),
+ status: response.message.toolCalls ? 'calling_tools' : 'idle',
+ };
+
+ onUpdate(currentConversation);
+
+ // Check if we have tool calls to execute
+ if (!response.message.toolCalls || response.message.toolCalls.length === 0) {
+ // No tool calls, we're done
+ break;
+ }
+
+ // Execute tool calls
+ loopState.currentStep = 'tool_execution';
+ currentConversation.status = 'calling_tools';
+ onUpdate(currentConversation);
+
+ const toolResults: ToolResultBlock[] = [];
+
+ for (const toolCall of response.message.toolCalls) {
+ if (abortController.signal.aborted) {
+ break;
+ }
+
+ const { result, error } = await executeTool(toolCall);
+
+ const toolResult: ToolResultBlock = {
+ type: 'tool_result',
+ tool_use_id: toolCall.id,
+ content: error
+ ? [{ type: 'error', error }]
+ : [{ type: 'text', text: JSON.stringify(result, null, 2) }],
+ is_error: !!error,
+ };
+
+ toolResults.push(toolResult);
+ }
+
+ // Add tool results as separate messages
+ const toolResultMessages: ConversationMessage[] = toolResults.map(result => ({
+ id: uuidv4(),
+ role: 'tool' as const,
+ content: [result],
+ timestamp: new Date(),
+ toolCallId: result.tool_use_id,
+ }));
+
+ currentConversation = {
+ ...currentConversation,
+ messages: [...currentConversation.messages, ...toolResultMessages],
+ status: 'thinking',
+ updatedAt: new Date(),
+ };
+
+ onUpdate(currentConversation);
+
+ // Continue the loop to process tool results
+ }
+
+ // Mark as complete
+ loopState.currentStep = 'complete';
+ loopState.isRunning = false;
+
+ if (currentConversation) {
+ currentConversation.status = 'idle';
+ onUpdate(currentConversation);
+ }
+
+ } catch (error) {
+ loopState.error = error instanceof Error ? error.message : 'Agent loop failed';
+ loopState.isRunning = false;
+
+ if (finalConfig.stopOnError) {
+ throw error;
+ }
+ } finally {
+ loopStates.current.delete(conversationId);
+ }
+ }, [currentProvider, finalConfig, toInferenceMessages, fromInferenceResponse, getAllTools, executeTool]);
+
+ const stopLoop = useCallback((conversationId: string) => {
+ const loopState = loopStates.current.get(conversationId);
+ if (loopState?.abortController) {
+ loopState.abortController.abort();
+ loopState.isRunning = false;
+ }
+ }, []);
+
+ const getLoopState = useCallback((conversationId: string): AgentLoopState | undefined => {
+ return loopStates.current.get(conversationId);
+ }, []);
+
+ return {
+ executeAgentLoop,
+ stopLoop,
+ getLoopState,
+ };
+}
\ No newline at end of file
diff --git a/src/types/conversation.ts b/src/types/conversation.ts
new file mode 100644
index 0000000..3485bd0
--- /dev/null
+++ b/src/types/conversation.ts
@@ -0,0 +1,126 @@
+// Conversation and agent loop types that extend existing inference types
+
+import type { ChatMessage, ToolCall, Tool } from './inference';
+
+// Extend ContentBlock to support tool use and tool result blocks
+export interface ToolUseBlock {
+ type: 'tool_use';
+ id: string;
+ name: string;
+ input: Record;
+}
+
+export interface ToolResultBlock {
+ type: 'tool_result';
+ tool_use_id: string;
+ content: string | { type: 'text' | 'image' | 'error'; text?: string; image?: string; error?: string }[];
+ is_error?: boolean;
+}
+
+export interface TextBlock {
+ type: 'text';
+ text: string;
+}
+
+// Union of all content block types
+export type ConversationContentBlock = TextBlock | ToolUseBlock | ToolResultBlock;
+
+// Extend ChatMessage for conversation persistence with structured content
+export interface ConversationMessage extends Omit {
+ id: string;
+ content: ConversationContentBlock[];
+ timestamp: Date;
+}
+
+export interface Conversation {
+ id: string;
+ title: string;
+ messages: ConversationMessage[];
+ createdAt: Date;
+ updatedAt: Date;
+ status: 'idle' | 'thinking' | 'calling_tools' | 'error';
+ error?: string;
+ // Track ongoing tool calls for UI display
+ pendingToolCalls?: {
+ id: string;
+ name: string;
+ input: Record;
+ startedAt: Date;
+ }[];
+}
+
+export interface AgentLoopState {
+ isRunning: boolean;
+ conversationId: string;
+ currentStep: 'inference' | 'tool_execution' | 'complete';
+ iteration: number;
+ maxIterations: number;
+ error?: string;
+ // AbortController for stopping the loop
+ abortController?: AbortController;
+}
+
+export interface ConversationContextValue {
+ // Conversation management
+ conversations: Conversation[];
+ activeConversationId?: string;
+ agentLoopStates: Map;
+
+ // Conversation operations
+ createConversation: (title?: string) => string; // Returns conversation ID
+ deleteConversation: (conversationId: string) => void;
+ setActiveConversation: (conversationId: string) => void;
+ updateConversationTitle: (conversationId: string, title: string) => void;
+ getConversation: (conversationId: string) => Conversation | undefined;
+
+ // Message operations
+ addUserMessage: (conversationId: string, content: string) => void;
+
+ // Agent loop operations
+ sendMessage: (conversationId: string, content: string) => Promise;
+ continueConversation: (conversationId: string) => Promise; // Continue after tool calls or follow-ups
+ stopAgentLoop: (conversationId: string) => void;
+ getAgentLoopState: (conversationId: string) => AgentLoopState | undefined;
+}
+
+export interface AgentLoopConfig {
+ maxIterations: number;
+ systemMessage?: string;
+ temperature?: number;
+ stopOnError: boolean;
+}
+
+// Test tools that can be used alongside MCP tools
+export interface TestTool extends Tool {
+ execute: (args: Record) => Promise;
+}
+
+// Hook interface for the agent loop
+export interface UseAgentLoopReturn {
+ executeAgentLoop: (
+ conversation: Conversation,
+ onUpdate: (conversation: Conversation) => void
+ ) => Promise;
+ stopLoop: (conversationId: string) => void;
+ getLoopState: (conversationId: string) => AgentLoopState | undefined;
+}
+
+export type AgentLoopHook = (config?: Partial) => UseAgentLoopReturn;
+
+// Helper functions for converting between formats
+export interface ConversationHelpers {
+ // Convert conversation messages to inference messages
+ toInferenceMessages: (messages: ConversationMessage[]) => ChatMessage[];
+
+ // Convert inference response to conversation message
+ fromInferenceResponse: (response: ChatMessage) => ConversationMessage;
+
+ // Create tool use blocks from tool calls
+ createToolUseBlocks: (toolCalls: ToolCall[]) => ToolUseBlock[];
+
+ // Create tool result blocks from tool execution
+ createToolResultBlocks: (results: { toolCallId: string; result: any; error?: string }[]) => ToolResultBlock[];
+
+ // Generate conversation title from first message
+ generateConversationTitle: (firstMessage: string) => string;
+}
\ No newline at end of file
From aed7a25ea79d757d851a952af930d087edc43308 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Wed, 11 Jun 2025 18:26:06 +0100
Subject: [PATCH 04/33] Fix authentication state consistency across components
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Fix ChatInterface to use context's reactive isAuthenticated value
- Fix useAgentLoop to use context's reactive isAuthenticated value
- Ensure consistent authentication state checking across the application
- Add isAuthenticated to useCallback dependencies for proper reactivity
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/components/ChatInterface.tsx | 2 +-
src/hooks/useAgentLoop.ts | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/components/ChatInterface.tsx b/src/components/ChatInterface.tsx
index e1bf3eb..caf19ec 100644
--- a/src/components/ChatInterface.tsx
+++ b/src/components/ChatInterface.tsx
@@ -16,7 +16,7 @@ export function ChatInterface() {
getAgentLoopState,
} = useConversation();
- const { currentProvider } = useInference();
+ const { provider: currentProvider, isAuthenticated } = useInference();
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef(null);
diff --git a/src/hooks/useAgentLoop.ts b/src/hooks/useAgentLoop.ts
index 53eac38..78e9ab6 100644
--- a/src/hooks/useAgentLoop.ts
+++ b/src/hooks/useAgentLoop.ts
@@ -126,7 +126,7 @@ const DEFAULT_CONFIG: AgentLoopConfig = {
export function useAgentLoop(config: Partial = {}): UseAgentLoopReturn {
const finalConfig = { ...DEFAULT_CONFIG, ...config };
- const { currentProvider } = useInference();
+ const { provider: currentProvider, isAuthenticated } = useInference();
const { getAllTools, callTool: callMCPTool, connections } = useMCP();
// Track running loops
@@ -255,7 +255,7 @@ export function useAgentLoop(config: Partial = {}): UseAgentLoo
conversation: Conversation,
onUpdate: (conversation: Conversation) => void
) => {
- if (!currentProvider?.isAuthenticated) {
+ if (!isAuthenticated || !currentProvider) {
throw new Error('No authenticated inference provider available');
}
@@ -397,7 +397,7 @@ export function useAgentLoop(config: Partial = {}): UseAgentLoo
} finally {
loopStates.current.delete(conversationId);
}
- }, [currentProvider, finalConfig, toInferenceMessages, fromInferenceResponse, getAllTools, executeTool]);
+ }, [isAuthenticated, currentProvider, finalConfig, toInferenceMessages, fromInferenceResponse, getAllTools, executeTool]);
const stopLoop = useCallback((conversationId: string) => {
const loopState = loopStates.current.get(conversationId);
From e689acee534e1ed77c282915d1dbe1f0d75243c9 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Wed, 11 Jun 2025 18:28:06 +0100
Subject: [PATCH 05/33] Add comprehensive conversation system design
documentation
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Document conversation system architecture and data models
- Explain agent loop implementation and tool integration
- Cover state management, authentication flow, and UI components
- Provide development guidelines and troubleshooting guide
- Include performance, security, and testing considerations
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
docs/conversation_system_design.md | 400 +++++++++++++++++++++++++++++
1 file changed, 400 insertions(+)
create mode 100644 docs/conversation_system_design.md
diff --git a/docs/conversation_system_design.md b/docs/conversation_system_design.md
new file mode 100644
index 0000000..521dbf6
--- /dev/null
+++ b/docs/conversation_system_design.md
@@ -0,0 +1,400 @@
+# Conversation System & Agent Loop Architecture
+
+## Overview
+
+The conversation system implements a complete conversational AI interface with tool calling capabilities, multi-conversation management, and persistence. It integrates with both inference providers (OpenRouter) and MCP (Model Context Protocol) servers to provide a unified tool ecosystem.
+
+## System Architecture
+
+### Core Components
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ ConversationApp │
+├─────────────────────┬───────────────────────────────────────┤
+│ ConversationSidebar│ ChatInterface │
+│ - Conversation List│ - MessageList │
+│ - MCP Status │ - MessageInput │
+│ - Management │ - Authentication │
+└─────────────────────┴───────────────────────────────────────┘
+```
+
+### Context Providers
+
+The system uses a layered context provider architecture:
+
+```typescript
+ // Authentication & model management
+ // MCP server connections
+ // Conversation state & agent loops
+
+
+
+
+```
+
+## Data Models
+
+### Conversation Structure
+
+```typescript
+interface Conversation {
+ id: string;
+ title: string;
+ messages: ConversationMessage[];
+ createdAt: Date;
+ updatedAt: Date;
+ status: 'idle' | 'thinking' | 'calling_tools' | 'error';
+ error?: string;
+ pendingToolCalls?: PendingToolCall[];
+}
+```
+
+### Message Format
+
+Messages use a structured content block system that supports text, tool use, and tool results:
+
+```typescript
+interface ConversationMessage {
+ id: string;
+ role: 'user' | 'assistant' | 'tool';
+ content: ConversationContentBlock[];
+ timestamp: Date;
+}
+
+type ConversationContentBlock =
+ | { type: 'text'; text: string }
+ | { type: 'tool_use'; id: string; name: string; input: Record }
+ | { type: 'tool_result'; tool_use_id: string; content: string | ContentItem[]; is_error?: boolean }
+```
+
+This design aligns with the Anthropic/Claude message format and supports rich tool interactions.
+
+## Agent Loop Implementation
+
+### Hook Architecture
+
+The agent loop is implemented as a custom React hook (`useAgentLoop`) that provides:
+
+```typescript
+interface UseAgentLoopReturn {
+ executeAgentLoop: (conversation: Conversation, onUpdate: (conversation: Conversation) => void) => Promise;
+ stopLoop: (conversationId: string) => void;
+ getLoopState: (conversationId: string) => AgentLoopState | undefined;
+}
+```
+
+### Execution Flow
+
+1. **Conversation Processing**: Takes a full conversation and processes the latest user message
+2. **Tool Discovery**: Aggregates tools from test tools and connected MCP servers
+3. **Inference Request**: Sends messages + available tools to the inference provider
+4. **Tool Execution**: Executes any tool calls returned by the model
+5. **Result Integration**: Adds tool results back to the conversation
+6. **Iteration**: Continues until no more tool calls or max iterations reached
+
+```typescript
+for (let iteration = 0; iteration < maxIterations; iteration++) {
+ // 1. Convert conversation to inference format
+ const inferenceMessages = toInferenceMessages(conversation.messages);
+
+ // 2. Get available tools (test + MCP)
+ const allTools = [...testTools, ...mcpTools];
+
+ // 3. Generate response with tools
+ const response = await currentProvider.generateResponse({
+ messages: inferenceMessages,
+ tools: allTools
+ });
+
+ // 4. Add assistant message to conversation
+ conversation.messages.push(fromInferenceResponse(response.message));
+
+ // 5. Execute tool calls if present
+ if (response.message.toolCalls) {
+ for (const toolCall of response.message.toolCalls) {
+ const result = await executeTool(toolCall);
+ conversation.messages.push(createToolResultMessage(result));
+ }
+ } else {
+ break; // No more tool calls, done
+ }
+}
+```
+
+### Tool Integration
+
+#### Test Tools
+Built-in tools for development and testing:
+- `get_weather`: Mock weather data
+- `calculate`: Basic arithmetic evaluation
+- `get_current_time`: Current timestamp with timezone support
+
+#### MCP Tools
+Tools from connected MCP servers are:
+- **Name-prefixed**: `${serverName}.${toolName}` to avoid conflicts
+- **Dynamically routed**: Tool calls extract server name and route to correct connection
+- **Auto-discovered**: Available tools update when servers connect/disconnect
+
+```typescript
+// Tool execution routing
+if (toolCall.function.name.includes('.')) {
+ const [serverName] = toolCall.function.name.split('.');
+ const connection = connections.find(conn => conn.name === serverName);
+ return await callMCPTool(connection.id, toolCall.function.name, toolCall.function.arguments);
+}
+```
+
+## State Management
+
+### Conversation Persistence
+
+Conversations are persisted to `localStorage` with automatic serialization/deserialization:
+
+```typescript
+// Save on changes
+useEffect(() => {
+ if (hasLoadedPersisted.current && conversations.length > 0) {
+ localStorage.setItem('conversations', JSON.stringify(conversations));
+ }
+}, [conversations]);
+
+// Load on mount
+useEffect(() => {
+ const persistedData = localStorage.getItem('conversations');
+ if (persistedData) {
+ const restored = JSON.parse(persistedData).map(conv => ({
+ ...conv,
+ createdAt: new Date(conv.createdAt),
+ updatedAt: new Date(conv.updatedAt),
+ messages: conv.messages.map(msg => ({
+ ...msg,
+ timestamp: new Date(msg.timestamp)
+ }))
+ }));
+ setConversations(restored);
+ }
+}, []);
+```
+
+### Authentication State Management
+
+The system uses a reactive authentication pattern to ensure UI consistency:
+
+```typescript
+// InferenceContext tracks auth state changes
+const [authStateVersion, setAuthStateVersion] = useState(0);
+
+const refreshAuthState = useCallback(() => {
+ setAuthStateVersion(prev => prev + 1);
+}, []);
+
+// Components use context's reactive isAuthenticated value
+const contextValue = {
+ isAuthenticated: provider?.isAuthenticated || false,
+ refreshAuthState,
+ // ...
+};
+```
+
+**Critical Pattern**: Always use `isAuthenticated` from context, not `provider?.isAuthenticated` directly, to ensure reactive updates.
+
+## User Interface Components
+
+### ConversationSidebar
+- **Conversation list** with title, timestamp, status indicators
+- **New conversation** creation
+- **Conversation management** (delete, switch active)
+- **MCP status toggle** for debugging
+
+### ChatInterface
+- **Authentication gate**: Shows inline login if not authenticated
+- **Message display** with tool call visualization
+- **Input handling** with disabled states
+- **Real-time status** showing agent loop progress
+
+### MessageList
+- **Role-based styling** (user, assistant, tool)
+- **Tool call blocks** showing tool name and input parameters
+- **Tool result blocks** with success/error states and formatted output
+- **Timestamp display** with relative formatting
+
+### MessageInput
+- **Auto-resize textarea** with Enter/Shift+Enter handling
+- **Send button** with proper disabled states and tooltips
+- **Character count** for long messages
+
+## Authentication Flow
+
+### Inline Authentication
+Users authenticate directly in the chat interface without leaving the conversation view:
+
+1. **Provider Selection**: Choose between API key and OAuth providers
+2. **Authentication**: Enter API key or complete OAuth flow
+3. **Model Loading**: Automatically load available models
+4. **Context Update**: Refresh authentication state across all components
+5. **Chat Enablement**: Message input becomes active
+
+```typescript
+// After successful authentication
+await selectedProvider.authenticate({ type: 'api_key', apiKey });
+await selectedProvider.loadModels();
+setProvider(selectedProvider);
+refreshAuthState(); // Critical: Force reactive update
+```
+
+## Development Guidelines
+
+### Adding New Tools
+
+1. **Test Tools**: Add to `testTools` array in `useAgentLoop.ts`
+2. **MCP Tools**: Connect MCP server via MCP Provider Test tab
+
+```typescript
+const newTestTool: TestTool = {
+ type: 'function',
+ function: {
+ name: 'my_tool',
+ description: 'Description of what the tool does',
+ parameters: {
+ type: 'object',
+ properties: {
+ param1: { type: 'string', description: 'Parameter description' }
+ },
+ required: ['param1']
+ }
+ },
+ execute: async (args) => {
+ // Tool implementation
+ return { result: 'tool output' };
+ }
+};
+```
+
+### Adding New Inference Providers
+
+1. **Implement InferenceProvider abstract class**
+2. **Add to available providers** in `InferenceLogin` component
+3. **Handle authentication methods** (API key, OAuth)
+
+### Message Format Extensions
+
+To add new content block types:
+
+1. **Extend ConversationContentBlock union type**
+2. **Add rendering logic** in `MessageList.renderContentBlock()`
+3. **Update conversion helpers** in `useAgentLoop`
+
+### Debugging
+
+Use the MCP Status panel (toggle in sidebar) to monitor:
+- **Server connections** and their status
+- **Available tools** from each server
+- **Connection errors** and retry attempts
+
+Debug conversation state in browser DevTools:
+- **localStorage['conversations']**: Persisted conversation data
+- **React DevTools**: Component state and context values
+
+## Error Handling
+
+### Agent Loop Errors
+- **Tool execution failures**: Captured and shown as error tool results
+- **Network failures**: Retry with exponential backoff
+- **Authentication failures**: Graceful fallback to login screen
+- **Max iterations**: Configurable limit to prevent infinite loops
+
+### State Recovery
+- **Conversation persistence**: Automatic save/restore across sessions
+- **Connection restoration**: MCP servers automatically reconnect on app load
+- **Authentication persistence**: Tokens stored securely in localStorage
+
+## Performance Considerations
+
+### Optimization Strategies
+- **Message virtualization**: Consider for very long conversations
+- **Tool result caching**: Avoid re-executing identical tool calls
+- **Connection pooling**: Reuse MCP connections across conversations
+- **Lazy loading**: Load conversation history on demand
+
+### Memory Management
+- **Conversation limits**: Consider archiving old conversations
+- **Message trimming**: Truncate very long conversations for inference
+- **Tool result size**: Limit large tool outputs in UI display
+
+## Security Considerations
+
+### Authentication
+- **API keys**: Stored in localStorage, never sent to third parties
+- **OAuth tokens**: Proper PKCE flow with secure token storage
+- **MCP connections**: Support both authenticated and unauthenticated servers
+
+### Tool Safety
+- **Input validation**: Sanitize tool inputs before execution
+- **Output sanitization**: Escape tool outputs in UI display
+- **Execution limits**: Prevent long-running or resource-intensive tools
+
+### Data Privacy
+- **Local storage**: All conversation data stays on client
+- **No analytics**: No conversation data sent to analytics services
+- **MCP isolation**: Each server connection is isolated
+
+## Future Enhancements
+
+### Planned Features
+- **Streaming responses**: Real-time message generation
+- **File attachments**: Support for images and documents
+- **Voice input/output**: Speech-to-text and text-to-speech
+- **Advanced tool calling**: Parallel tool execution, tool composition
+- **Conversation export**: Export conversations to various formats
+- **Collaborative editing**: Shared conversations between users
+
+### Architecture Improvements
+- **Message streaming**: WebSocket-based real-time updates
+- **Background processing**: Web Workers for tool execution
+- **Offline support**: Service Worker for offline conversation access
+- **Database migration**: Move from localStorage to IndexedDB
+- **Plugin system**: Third-party tool and provider extensions
+
+## Testing Strategy
+
+### Unit Tests
+- **Tool execution**: Verify tool calling and result handling
+- **Message formatting**: Test content block serialization/deserialization
+- **Authentication flows**: Mock provider authentication
+
+### Integration Tests
+- **Agent loop**: End-to-end conversation flow with tool calls
+- **MCP integration**: Real MCP server connections and tool execution
+- **Persistence**: Conversation save/restore functionality
+
+### User Testing
+- **Authentication UX**: Smooth login flow without friction
+- **Tool usage**: Intuitive tool call visualization and results
+- **Multi-conversation**: Easy conversation management and switching
+
+## Troubleshooting Guide
+
+### Common Issues
+
+**"No authenticated inference provider available"**
+- Verify `isAuthenticated` from context, not `provider?.isAuthenticated`
+- Ensure `refreshAuthState()` called after authentication
+- Check if provider is properly set in context
+
+**Tool calls not working**
+- Verify MCP server connections in MCP Status panel
+- Check tool name prefixing for MCP tools (`server.tool_name`)
+- Ensure tool schemas match expected format
+
+**Conversations not persisting**
+- Check localStorage quota and permissions
+- Verify conversation serialization doesn't fail
+- Check browser's localStorage support
+
+**UI not updating after authentication**
+- Ensure components use context's `isAuthenticated` value
+- Verify `refreshAuthState()` is called after auth changes
+- Check React DevTools for proper context updates
+
+This system provides a robust foundation for conversational AI with extensible tool calling capabilities, proper state management, and a clean user experience.
\ No newline at end of file
From 0a06544bda99bf2874089548af6778bec06f3de7 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 12 Jun 2025 12:06:06 +0100
Subject: [PATCH 06/33] Fix MCP tool naming for OpenRouter API compatibility
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Change tool name separator from dot to double underscore (server__tool)
- Update tool discovery to use server__tool_name format
- Update tool execution routing to parse double underscore separator
- Fix OpenRouter API error: tool names must match pattern '^[a-zA-Z0-9_-]{1,64}'
This resolves the silent failure when MCP tools were present in conversations.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/hooks/useAgentLoop.ts | 8 ++++----
src/mcp/connection.ts | 9 +++++----
2 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/src/hooks/useAgentLoop.ts b/src/hooks/useAgentLoop.ts
index 78e9ab6..7c2e6bc 100644
--- a/src/hooks/useAgentLoop.ts
+++ b/src/hooks/useAgentLoop.ts
@@ -225,10 +225,10 @@ export function useAgentLoop(config: Partial = {}): UseAgentLoo
return { result };
}
- // Check if it's an MCP tool (prefixed with server name)
- if (toolCall.function.name.includes('.')) {
- // Extract server name from tool name (format: "server.tool_name")
- const [serverName] = toolCall.function.name.split('.');
+ // Check if it's an MCP tool (prefixed with server name using double underscore)
+ if (toolCall.function.name.includes('__')) {
+ // Extract server name from tool name (format: "server__tool_name")
+ const [serverName] = toolCall.function.name.split('__');
// Find the connection ID for this server name
const connection = connections.find(conn => conn.name === serverName);
diff --git a/src/mcp/connection.ts b/src/mcp/connection.ts
index 93b0f8a..fde9646 100644
--- a/src/mcp/connection.ts
+++ b/src/mcp/connection.ts
@@ -581,10 +581,11 @@ export class MCPConnectionManager implements MCPConnectionManager {
const result = await this.client.listTools();
// Transform MCP tools to our Tool interface with name prefixing
+ // Use double underscore instead of dot to comply with OpenRouter API requirements
return result.tools.map(tool => ({
type: 'function' as const,
function: {
- name: `${this.connection.name}.${tool.name}`,
+ name: `${this.connection.name}__${tool.name}`,
description: `[${this.connection.name}] ${tool.description || ''}`,
parameters: tool.inputSchema || {},
},
@@ -636,9 +637,9 @@ export class MCPConnectionManager implements MCPConnectionManager {
throw new Error('Client not connected');
}
- // Remove the server prefix from the tool name
- const unprefixedName = toolName.startsWith(`${this.connection.name}.`)
- ? toolName.slice(this.connection.name.length + 1)
+ // Remove the server prefix from the tool name (using double underscore separator)
+ const unprefixedName = toolName.startsWith(`${this.connection.name}__`)
+ ? toolName.slice(this.connection.name.length + 2)
: toolName;
try {
From d028846a145b0e29e89455ce1e8cb3143e3d9139 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 12 Jun 2025 12:08:10 +0100
Subject: [PATCH 07/33] Update conversation system documentation for tool
naming fix
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Update MCP tool naming from server.tool to server__tool format
- Add tool naming requirements section with OpenRouter API constraints
- Update troubleshooting guide with tool naming validation errors
- Add specific troubleshooting for agent loop hanging with MCP tools
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
docs/conversation_system_design.md | 28 ++++++++++++++++++++++++----
1 file changed, 24 insertions(+), 4 deletions(-)
diff --git a/docs/conversation_system_design.md b/docs/conversation_system_design.md
index 521dbf6..fb85a88 100644
--- a/docs/conversation_system_design.md
+++ b/docs/conversation_system_design.md
@@ -132,19 +132,32 @@ Built-in tools for development and testing:
#### MCP Tools
Tools from connected MCP servers are:
-- **Name-prefixed**: `${serverName}.${toolName}` to avoid conflicts
+- **Name-prefixed**: `${serverName}__${toolName}` to avoid conflicts and comply with API requirements
- **Dynamically routed**: Tool calls extract server name and route to correct connection
- **Auto-discovered**: Available tools update when servers connect/disconnect
```typescript
// Tool execution routing
-if (toolCall.function.name.includes('.')) {
- const [serverName] = toolCall.function.name.split('.');
+if (toolCall.function.name.includes('__')) {
+ const [serverName] = toolCall.function.name.split('__');
const connection = connections.find(conn => conn.name === serverName);
return await callMCPTool(connection.id, toolCall.function.name, toolCall.function.arguments);
}
```
+#### Tool Naming Requirements
+
+**Important**: Tool names must comply with OpenRouter API requirements:
+- **Pattern**: Must match `^[a-zA-Z0-9_-]{1,64}$`
+- **No dots allowed**: This is why we use double underscore (`__`) as separator
+- **Length limit**: Maximum 64 characters total
+
+Example tool names:
+- ✅ `test__echo` (valid)
+- ✅ `weather_service__get_forecast` (valid)
+- ❌ `test.echo` (invalid - contains dot)
+- ❌ `server__very_long_tool_name_that_exceeds_the_sixty_four_character_limit` (invalid - too long)
+
## State Management
### Conversation Persistence
@@ -384,8 +397,9 @@ Debug conversation state in browser DevTools:
**Tool calls not working**
- Verify MCP server connections in MCP Status panel
-- Check tool name prefixing for MCP tools (`server.tool_name`)
+- Check tool name prefixing for MCP tools (`server__tool_name`)
- Ensure tool schemas match expected format
+- Verify tool names comply with API requirements (alphanumeric, underscore, hyphen only)
**Conversations not persisting**
- Check localStorage quota and permissions
@@ -397,4 +411,10 @@ Debug conversation state in browser DevTools:
- Verify `refreshAuthState()` is called after auth changes
- Check React DevTools for proper context updates
+**Agent loop hangs with MCP tools connected**
+- Check browser console for OpenRouter API errors about tool names
+- Verify tool names don't contain dots or other invalid characters
+- Ensure tool names are under 64 characters total length
+- Look for pattern validation errors: `String should match pattern '^[a-zA-Z0-9_-]{1,64}'`
+
This system provides a robust foundation for conversational AI with extensible tool calling capabilities, proper state management, and a clean user experience.
\ No newline at end of file
From 2038ea595716c65256ff3538c0e4381f7e230614 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 12 Jun 2025 13:38:20 +0100
Subject: [PATCH 08/33] Implement auth token persistence and MCP connection
improvements
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add OpenRouter API key persistence to localStorage with auto-restore
- Add automatic provider restoration on app startup for seamless login
- Fix MCP OAuth token persistence by preserving connection IDs across refreshes
- Add manual reconnect button for failed/disconnected MCP servers
- Add auto-reconnect for MCP servers on app startup
- Maintain backward compatibility with old connection storage format
Resolves auth token loss on page refresh for both inference and MCP providers.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/components/MCPStatus.tsx | 12 ++++++-
src/contexts/InferenceContext.tsx | 25 ++++++++++++++-
src/contexts/MCPContext.tsx | 40 +++++++++++++++++++-----
src/providers/openrouter/api-provider.ts | 29 +++++++++++++++++
4 files changed, 96 insertions(+), 10 deletions(-)
diff --git a/src/components/MCPStatus.tsx b/src/components/MCPStatus.tsx
index 819d34b..6c2bc54 100644
--- a/src/components/MCPStatus.tsx
+++ b/src/components/MCPStatus.tsx
@@ -4,7 +4,7 @@ import React from 'react';
import { useMCP } from '@/contexts/MCPContext';
export function MCPStatus() {
- const { connections, getAllTools } = useMCP();
+ const { connections, getAllTools, reconnectServer } = useMCP();
const connectedServers = connections.filter(conn => conn.status === 'connected');
const allTools = getAllTools();
@@ -89,6 +89,16 @@ export function MCPStatus() {
)}
+
+ {(connection.status === 'failed' || connection.status === 'disconnected') && (
+ reconnectServer(connection.id)}
+ className="ml-2 px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200 rounded hover:bg-blue-200 dark:hover:bg-blue-800"
+ title="Reconnect server"
+ >
+ Reconnect
+
+ )}
{connection.tools.length > 0 && (
diff --git a/src/contexts/InferenceContext.tsx b/src/contexts/InferenceContext.tsx
index c7246c8..04cf371 100644
--- a/src/contexts/InferenceContext.tsx
+++ b/src/contexts/InferenceContext.tsx
@@ -1,12 +1,14 @@
// React context for inference provider management
-import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
+import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react';
import type {
InferenceProvider,
InferenceRequest,
InferenceResponse,
Model,
} from '@/types/inference';
+import { OpenRouterApiProvider } from '@/providers/openrouter/api-provider';
+import { OpenRouterOAuthProvider } from '@/providers/openrouter/oauth-provider';
interface InferenceContextValue {
// Current provider state
@@ -64,6 +66,27 @@ export function InferenceProvider({ children }: InferenceProviderProps) {
setAuthStateVersion(prev => prev + 1);
}, []);
+ // Auto-restore provider with stored credentials on mount
+ useEffect(() => {
+ const tryRestoreProvider = async () => {
+ // Try API provider first
+ const apiProvider = new OpenRouterApiProvider();
+ if (apiProvider.isAuthenticated) {
+ setProvider(apiProvider);
+ return;
+ }
+
+ // Try OAuth provider
+ const oauthProvider = new OpenRouterOAuthProvider();
+ if (oauthProvider.isAuthenticated) {
+ setProvider(oauthProvider);
+ return;
+ }
+ };
+
+ tryRestoreProvider().catch(console.error);
+ }, [setProvider]);
+
const generateResponse = useCallback(async (request: InferenceRequest): Promise => {
if (!provider) {
throw new Error('No inference provider configured');
diff --git a/src/contexts/MCPContext.tsx b/src/contexts/MCPContext.tsx
index 7794688..3f6b47b 100644
--- a/src/contexts/MCPContext.tsx
+++ b/src/contexts/MCPContext.tsx
@@ -39,22 +39,42 @@ export function MCPProvider({ children }: MCPProviderProps) {
try {
const persistedData = localStorage.getItem('mcp_connections');
if (persistedData) {
- const persistedConfigs: MCPServerConfig[] = JSON.parse(persistedData);
+ const persistedData_parsed = JSON.parse(persistedData);
- // Restore connections directly without using addMcpServer to avoid loops
- for (const config of persistedConfigs) {
+ // Handle both old format (array of configs) and new format (array of {id, config})
+ const persistedConnections = Array.isArray(persistedData_parsed) && persistedData_parsed.length > 0
+ ? (typeof persistedData_parsed[0] === 'object' && 'config' in persistedData_parsed[0]
+ ? persistedData_parsed as {id: string, config: MCPServerConfig}[]
+ : persistedData_parsed.map((config: MCPServerConfig) => ({id: uuidv4(), config})))
+ : [];
+
+ // Restore connections and auto-reconnect
+ for (const {id: connectionId, config} of persistedConnections) {
try {
- const connectionId = uuidv4();
const manager = new MCPConnectionManager(connectionId, config);
+ // Set up callback for connection state updates
+ manager.setConnectionUpdateCallback(() => {
+ setConnections(prev =>
+ prev.map(conn =>
+ conn.id === connectionId ? manager.getConnection() : conn
+ )
+ );
+ });
+
// Add to managers map
setManagers(prev => new Map(prev).set(connectionId, manager));
// Add initial connection state
setConnections(prev => [...prev, manager.getConnection()]);
- // Don't auto-connect during restoration - let user manually connect
- console.log(`Restored connection config for ${config.name}`);
+ // Auto-connect on restoration
+ try {
+ await manager.connect();
+ console.log(`Auto-reconnected to ${config.name}`);
+ } catch (error) {
+ console.warn(`Failed to auto-reconnect to ${config.name}:`, error);
+ }
} catch (error) {
console.warn(`Failed to restore connection to ${config.name}:`, error);
}
@@ -70,8 +90,12 @@ export function MCPProvider({ children }: MCPProviderProps) {
const persistConnections = useCallback(() => {
try {
- const configs = connections.map(conn => conn.config);
- localStorage.setItem('mcp_connections', JSON.stringify(configs));
+ // Store both config and connection ID to maintain OAuth token association
+ const connectionData = connections.map(conn => ({
+ id: conn.id,
+ config: conn.config
+ }));
+ localStorage.setItem('mcp_connections', JSON.stringify(connectionData));
} catch (error) {
console.error('Failed to persist MCP connections:', error);
}
diff --git a/src/providers/openrouter/api-provider.ts b/src/providers/openrouter/api-provider.ts
index ee444f8..34c98bf 100644
--- a/src/providers/openrouter/api-provider.ts
+++ b/src/providers/openrouter/api-provider.ts
@@ -31,6 +31,9 @@ export class OpenRouterApiProvider extends InferenceProvider {
if (config?.defaultModel) {
// We'll set this after loading models
}
+
+ // Load stored API key
+ this.loadStoredApiKey();
}
get isAuthenticated(): boolean {
@@ -60,6 +63,9 @@ export class OpenRouterApiProvider extends InferenceProvider {
try {
// Test the API key by loading models
await this.loadModels();
+
+ // Store API key after successful validation
+ this.storeApiKey();
} catch (error) {
this._authError = error instanceof Error ? error.message : 'Authentication failed';
this.apiKey = undefined;
@@ -72,6 +78,7 @@ export class OpenRouterApiProvider extends InferenceProvider {
this._authError = undefined;
this._models = [];
this._selectedModel = undefined;
+ this.clearStoredApiKey();
}
async loadModels(): Promise {
@@ -152,4 +159,26 @@ export class OpenRouterApiProvider extends InferenceProvider {
private isInferenceError(error: any): error is InferenceError {
return error && typeof error === 'object' && 'type' in error && 'message' in error;
}
+
+ private storeApiKey(): void {
+ if (this.apiKey) {
+ localStorage.setItem('openrouter_api_key', this.apiKey);
+ }
+ }
+
+ private loadStoredApiKey(): void {
+ const storedKey = localStorage.getItem('openrouter_api_key');
+ if (storedKey) {
+ this.apiKey = storedKey;
+ // Try to load models to verify the key is still valid
+ this.loadModels().catch(() => {
+ // If loading fails, clear invalid key
+ this.logout();
+ });
+ }
+ }
+
+ private clearStoredApiKey(): void {
+ localStorage.removeItem('openrouter_api_key');
+ }
}
\ No newline at end of file
From 763742ed307040c2496a8b685d500eed2fb6da7e Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 12 Jun 2025 14:02:00 +0100
Subject: [PATCH 09/33] Implement improved auto-reconnect logic and fix
TypeScript errors
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Features
- Add proactive health check monitoring for MCP connections (30s intervals)
- Implement automatic reconnection on health check failures
- Add proper cleanup of health check intervals on disconnect
## Fixes
- Resolve OAuth type compatibility issues with MCP SDK
- Fix naming collision between InferenceProvider class and React component
- Remove unused React imports across all components (new JSX transform)
- Clean up unused type imports and variables
- Remove unused handleOAuthAuthentication function
## Technical Details
- Health checks use lightweight listTools() calls to verify connection state
- Automatic reconnection triggers when health checks fail
- Maintains existing exponential backoff retry logic
- All TypeScript compilation errors resolved
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/App.tsx | 8 +-
src/components/ChatInterface.tsx | 4 +-
src/components/ConversationApp.tsx | 2 +-
src/components/ConversationSidebar.tsx | 10 +-
src/components/InferenceLogin.tsx | 2 +-
src/components/InferenceTest.tsx | 2 +-
src/components/MCPStatus.tsx | 1 -
src/components/MCPTest.tsx | 2 +-
src/components/MessageList.tsx | 3 +-
src/components/OAuthCallback.tsx | 2 +-
src/contexts/ConversationContext.tsx | 3 +-
src/contexts/InferenceContext.tsx | 6 +-
src/contexts/MCPContext.tsx | 11 +--
src/hooks/useAgentLoop.ts | 1 -
src/mcp/connection.ts | 129 ++++++++++++++++++-------
src/providers/openrouter/client.ts | 5 +-
tsconfig.json | 2 +-
17 files changed, 122 insertions(+), 71 deletions(-)
diff --git a/src/App.tsx b/src/App.tsx
index d1da0da..0d90bd1 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,5 +1,5 @@
-import React, { useState } from 'react'
-import { InferenceProvider } from '@/contexts/InferenceContext'
+import { useState } from 'react'
+import { InferenceContextProvider } from '@/contexts/InferenceContext'
import { MCPProvider } from '@/contexts/MCPContext'
import { InferenceTest } from '@/components/InferenceTest'
import { MCPTest } from '@/components/MCPTest'
@@ -23,7 +23,7 @@ function App() {
const [activeTab, setActiveTab] = useState<'conversations' | 'inference' | 'mcp'>('conversations');
return (
-
+
{/* Tab Navigation */}
@@ -70,7 +70,7 @@ function App() {
{activeTab === 'mcp' && }
-
+
)
}
diff --git a/src/components/ChatInterface.tsx b/src/components/ChatInterface.tsx
index caf19ec..74b7af5 100644
--- a/src/components/ChatInterface.tsx
+++ b/src/components/ChatInterface.tsx
@@ -1,6 +1,6 @@
// Main chat interface with message display and input
-import React, { useState, useRef, useEffect } from 'react';
+import { useState, useRef, useEffect } from 'react';
import { useConversation } from '@/contexts/ConversationContext';
import { useInference } from '@/contexts/InferenceContext';
import { MessageList } from './MessageList';
@@ -16,7 +16,7 @@ export function ChatInterface() {
getAgentLoopState,
} = useConversation();
- const { provider: currentProvider, isAuthenticated } = useInference();
+ const { provider: currentProvider } = useInference();
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef(null);
diff --git a/src/components/ConversationApp.tsx b/src/components/ConversationApp.tsx
index 86576aa..0463e89 100644
--- a/src/components/ConversationApp.tsx
+++ b/src/components/ConversationApp.tsx
@@ -1,6 +1,6 @@
// Main conversation application with sidebar and chat interface
-import React, { useState } from 'react';
+import { useState } from 'react';
import { ConversationProvider } from '@/contexts/ConversationContext';
import { ConversationSidebar } from './ConversationSidebar';
import { ChatInterface } from './ChatInterface';
diff --git a/src/components/ConversationSidebar.tsx b/src/components/ConversationSidebar.tsx
index ca2490d..094ef33 100644
--- a/src/components/ConversationSidebar.tsx
+++ b/src/components/ConversationSidebar.tsx
@@ -1,6 +1,5 @@
// Conversation sidebar with conversation list and management
-import React from 'react';
import { useConversation } from '@/contexts/ConversationContext';
export function ConversationSidebar() {
@@ -91,10 +90,11 @@ export function ConversationSidebar() {
{conversation.messages.length > 0 && (
- {conversation.messages[conversation.messages.length - 1]?.content?.[0]?.type === 'text'
- ? conversation.messages[conversation.messages.length - 1].content[0].text
- : 'Tool interaction'
- }
+ {(() => {
+ const lastMessage = conversation.messages[conversation.messages.length - 1];
+ const firstBlock = lastMessage?.content?.[0];
+ return firstBlock?.type === 'text' ? firstBlock.text : 'Tool interaction';
+ })()}
)}
diff --git a/src/components/InferenceLogin.tsx b/src/components/InferenceLogin.tsx
index dbb2163..431f319 100644
--- a/src/components/InferenceLogin.tsx
+++ b/src/components/InferenceLogin.tsx
@@ -1,6 +1,6 @@
// Inference provider login component for inline authentication
-import React, { useState, useMemo } from 'react';
+import { useState, useMemo } from 'react';
import { useInference } from '@/contexts/InferenceContext';
import { OpenRouterApiProvider } from '@/providers/openrouter/api-provider';
import { OpenRouterOAuthProvider } from '@/providers/openrouter/oauth-provider';
diff --git a/src/components/InferenceTest.tsx b/src/components/InferenceTest.tsx
index d246ed0..50d321d 100644
--- a/src/components/InferenceTest.tsx
+++ b/src/components/InferenceTest.tsx
@@ -1,6 +1,6 @@
// Test UI for inference provider functionality
-import React, { useState, useCallback } from 'react';
+import { useState, useCallback } from 'react';
import { useInference } from '@/contexts/InferenceContext';
import { OpenRouterApiProvider, OpenRouterOAuthProvider } from '@/providers/openrouter';
import type { ChatMessage, InferenceRequest } from '@/types/inference';
diff --git a/src/components/MCPStatus.tsx b/src/components/MCPStatus.tsx
index 6c2bc54..66a115e 100644
--- a/src/components/MCPStatus.tsx
+++ b/src/components/MCPStatus.tsx
@@ -1,6 +1,5 @@
// MCP status display for the conversation sidebar
-import React from 'react';
import { useMCP } from '@/contexts/MCPContext';
export function MCPStatus() {
diff --git a/src/components/MCPTest.tsx b/src/components/MCPTest.tsx
index 2636cc4..aa905a8 100644
--- a/src/components/MCPTest.tsx
+++ b/src/components/MCPTest.tsx
@@ -1,6 +1,6 @@
// Test UI for MCP provider functionality
-import React, { useState, useCallback } from 'react';
+import { useState, useCallback } from 'react';
import { useMCP } from '@/contexts/MCPContext';
import type { MCPServerConfig } from '@/types/mcp';
diff --git a/src/components/MessageList.tsx b/src/components/MessageList.tsx
index bfc9200..b3b1ee2 100644
--- a/src/components/MessageList.tsx
+++ b/src/components/MessageList.tsx
@@ -1,7 +1,6 @@
// Message list component to display conversation messages with tool calls
-import React from 'react';
-import type { ConversationMessage, ToolUseBlock, ToolResultBlock } from '@/types/conversation';
+import type { ConversationMessage } from '@/types/conversation';
interface MessageListProps {
messages: ConversationMessage[];
diff --git a/src/components/OAuthCallback.tsx b/src/components/OAuthCallback.tsx
index b96f3b8..e0c6507 100644
--- a/src/components/OAuthCallback.tsx
+++ b/src/components/OAuthCallback.tsx
@@ -1,6 +1,6 @@
// OAuth callback handler for popup-based OAuth flows
-import React, { useEffect } from 'react';
+import { useEffect } from 'react';
interface OAuthCallbackProps {
type: 'inference' | 'mcp';
diff --git a/src/contexts/ConversationContext.tsx b/src/contexts/ConversationContext.tsx
index d6ba3ec..a56560f 100644
--- a/src/contexts/ConversationContext.tsx
+++ b/src/contexts/ConversationContext.tsx
@@ -1,6 +1,6 @@
// React context for conversation management and agent loop orchestration
-import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode, useRef } from 'react';
+import { createContext, useContext, useState, useCallback, useEffect, ReactNode, useRef } from 'react';
import { v4 as uuidv4 } from 'uuid';
import type {
@@ -8,7 +8,6 @@ import type {
ConversationMessage,
ConversationContextValue,
AgentLoopState,
- ConversationContentBlock,
} from '@/types/conversation';
import { useAgentLoop } from '@/hooks/useAgentLoop';
diff --git a/src/contexts/InferenceContext.tsx b/src/contexts/InferenceContext.tsx
index 04cf371..15d2d33 100644
--- a/src/contexts/InferenceContext.tsx
+++ b/src/contexts/InferenceContext.tsx
@@ -1,6 +1,6 @@
// React context for inference provider management
-import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react';
+import { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react';
import type {
InferenceProvider,
InferenceRequest,
@@ -38,12 +38,12 @@ interface InferenceProviderProps {
children: ReactNode;
}
-export function InferenceProvider({ children }: InferenceProviderProps) {
+export function InferenceContextProvider({ children }: InferenceProviderProps) {
const [provider, setProviderState] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [selectedModelId, setSelectedModelId] = useState(undefined);
- const [authStateVersion, setAuthStateVersion] = useState(0); // Force re-renders on auth changes
+ const [_, setAuthStateVersion] = useState(0); // Force re-renders on auth changes
const setProvider = useCallback((newProvider: InferenceProvider) => {
setProviderState(newProvider);
diff --git a/src/contexts/MCPContext.tsx b/src/contexts/MCPContext.tsx
index 3f6b47b..71bc612 100644
--- a/src/contexts/MCPContext.tsx
+++ b/src/contexts/MCPContext.tsx
@@ -1,6 +1,6 @@
// React context for MCP server connection management
-import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode, useRef } from 'react';
+import { createContext, useContext, useState, useCallback, useEffect, ReactNode, useRef } from 'react';
import { v4 as uuidv4 } from 'uuid';
import type {
@@ -8,7 +8,6 @@ import type {
MCPServerConfig,
MCPResource,
MCPContextValue,
- MCPError,
} from '@/types/mcp';
import type { Tool } from '@/types/inference';
import { MCPConnectionManager } from '@/mcp/connection';
@@ -158,18 +157,14 @@ export function MCPProvider({ children }: MCPProviderProps) {
authType: config.authType,
errorType: typeof error,
errorConstructor: error?.constructor?.name,
- errorMessage: error?.message,
- errorDetails: error?.details,
- detailsConstructor: error?.details?.constructor?.name,
+ errorMessage: error instanceof Error ? error.message : 'Unknown error',
fullError: error
});
const isUnauthorizedError =
config.authType === 'oauth' && (
(error instanceof Error && error.message === 'Unauthorized') ||
- (error instanceof Error && error.constructor.name === 'UnauthorizedError') ||
- (error && typeof error === 'object' && error.message === 'Unauthorized' &&
- error.details && error.details.constructor && error.details.constructor.name === 'UnauthorizedError')
+ (error instanceof Error && error.constructor.name === 'UnauthorizedError')
);
if (isUnauthorizedError) {
diff --git a/src/hooks/useAgentLoop.ts b/src/hooks/useAgentLoop.ts
index 7c2e6bc..63e9e38 100644
--- a/src/hooks/useAgentLoop.ts
+++ b/src/hooks/useAgentLoop.ts
@@ -9,7 +9,6 @@ import type {
UseAgentLoopReturn,
ConversationMessage,
Conversation,
- ToolUseBlock,
ToolResultBlock,
TestTool,
} from '@/types/conversation';
diff --git a/src/mcp/connection.ts b/src/mcp/connection.ts
index fde9646..92ca4a9 100644
--- a/src/mcp/connection.ts
+++ b/src/mcp/connection.ts
@@ -5,14 +5,8 @@ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import {
- auth,
- discoverOAuthMetadata,
- extractResourceMetadataUrl,
- type OAuthClientProvider,
- type OAuthClientMetadata,
- type OAuthClientInformation,
- type OAuthTokens,
- type OAuthClientInformationFull
+ auth,
+ type OAuthClientProvider
} from '@modelcontextprotocol/sdk/client/auth.js';
import type {
@@ -20,7 +14,6 @@ import type {
MCPServerConfig,
MCPResource,
MCPPrompt,
- MCPConnectionManager,
MCPError,
} from '@/types/mcp';
import type { Tool } from '@/types/inference';
@@ -31,6 +24,33 @@ interface MCPOAuthState {
expiresAt: number;
}
+// OAuth types matching the MCP SDK's internal interfaces
+interface OAuthClientMetadata {
+ redirect_uris: string[];
+ grant_types?: string[];
+ response_types?: string[];
+ client_name?: string;
+ token_endpoint_auth_method?: string;
+ scope?: string;
+ jwks_uri?: string;
+}
+
+interface OAuthClientInformation {
+ client_id: string;
+ client_secret?: string;
+ registration_access_token?: string;
+ registration_client_uri?: string;
+ client_secret_expires_at?: number;
+}
+
+interface OAuthTokens {
+ access_token: string;
+ token_type: string;
+ refresh_token?: string;
+ expires_in?: number;
+ scope?: string;
+}
+
class MCPOAuthProvider implements OAuthClientProvider {
private connectionId: string;
private serverName: string;
@@ -85,7 +105,7 @@ class MCPOAuthProvider implements OAuthClientProvider {
return stored ? JSON.parse(stored) : undefined;
}
- async saveClientInformation(clientInformation: OAuthClientInformationFull): Promise {
+ async saveClientInformation(clientInformation: OAuthClientInformation): Promise {
console.log('Registered OAuth client for MCP server');
const serverKey = this.getServerKey();
localStorage.setItem(`mcp_oauth_client_${serverKey}`, JSON.stringify(clientInformation));
@@ -212,11 +232,12 @@ class MCPOAuthProvider implements OAuthClientProvider {
}
}
-export class MCPConnectionManager implements MCPConnectionManager {
+export class MCPConnectionManager {
private connection: MCPConnection;
private client?: Client;
private transport?: Transport;
private reconnectTimeout?: NodeJS.Timeout;
+ private healthCheckInterval?: NodeJS.Timeout;
private oauthProvider?: MCPOAuthProvider;
private onConnectionUpdate?: () => void;
@@ -309,6 +330,9 @@ export class MCPConnectionManager implements MCPConnectionManager {
this.connection.lastConnected = new Date();
this.connection.connectionAttempts = 0;
+ // Start health check monitoring
+ this.startHealthCheck();
+
} catch (error) {
this.connection.status = 'failed';
this.connection.error = error instanceof Error ? error.message : 'Unknown connection error';
@@ -339,6 +363,11 @@ export class MCPConnectionManager implements MCPConnectionManager {
this.reconnectTimeout = undefined;
}
+ if (this.healthCheckInterval) {
+ clearInterval(this.healthCheckInterval);
+ this.healthCheckInterval = undefined;
+ }
+
if (this.client) {
try {
await this.client.close();
@@ -441,30 +470,6 @@ export class MCPConnectionManager implements MCPConnectionManager {
}
}
- private async handleOAuthAuthentication(): Promise {
- if (!this.oauthProvider) {
- throw new Error('OAuth provider not initialized');
- }
-
- console.log('Starting OAuth authentication flow...');
-
- try {
- const result = await auth(this.oauthProvider, {
- serverUrl: this.connection.url,
- scope: this.connection.config.oauthConfig?.scope,
- });
-
- if (result === 'REDIRECT') {
- // OAuth flow was initiated via popup, no further action needed here
- console.log('OAuth flow initiated via popup');
- } else if (result === 'AUTHORIZED') {
- console.log('OAuth authentication successful');
- }
- } catch (error) {
- console.error('OAuth authentication failed:', error);
- throw new Error(`OAuth authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
- }
- }
private async tryStreamableHttp(): Promise {
try {
@@ -663,6 +668,60 @@ export class MCPConnectionManager implements MCPConnectionManager {
return Math.min(1000 * Math.pow(2, this.connection.connectionAttempts - 1), 16000);
}
+ private startHealthCheck(): void {
+ // Clear any existing health check
+ if (this.healthCheckInterval) {
+ clearInterval(this.healthCheckInterval);
+ }
+
+ // Start health check every 30 seconds
+ this.healthCheckInterval = setInterval(async () => {
+ await this.performHealthCheck();
+ }, 30000);
+
+ console.log(`Started health check monitoring for ${this.connection.name}`);
+ }
+
+ private async performHealthCheck(): Promise {
+ // Only check if we're supposed to be connected
+ if (this.connection.status !== 'connected' || !this.client) {
+ return;
+ }
+
+ try {
+ // Try to list tools as a health check - this is a lightweight operation
+ await this.client.listTools();
+ console.log(`Health check passed for ${this.connection.name}`);
+ } catch (error) {
+ console.warn(`Health check failed for ${this.connection.name}:`, error);
+ await this.handleHealthCheckFailure(error);
+ }
+ }
+
+ private async handleHealthCheckFailure(error: any): Promise {
+ console.log(`Connection health check failed for ${this.connection.name}, attempting reconnection...`);
+
+ // Stop health check during reconnection attempt
+ if (this.healthCheckInterval) {
+ clearInterval(this.healthCheckInterval);
+ this.healthCheckInterval = undefined;
+ }
+
+ // Mark as disconnected and attempt reconnection
+ this.connection.status = 'connecting';
+ this.connection.error = error instanceof Error ? error.message : 'Health check failed';
+ this.notifyConnectionUpdate();
+
+ try {
+ // Attempt reconnection
+ await this.connect();
+ console.log(`Health check reconnection successful for ${this.connection.name}`);
+ } catch (reconnectError) {
+ console.error(`Health check reconnection failed for ${this.connection.name}:`, reconnectError);
+ // The connect method will handle scheduling retry attempts
+ }
+ }
+
private createMCPError(type: MCPError['type'], message: string, details?: any): MCPError {
return {
type,
diff --git a/src/providers/openrouter/client.ts b/src/providers/openrouter/client.ts
index 6de1fda..f26fc0f 100644
--- a/src/providers/openrouter/client.ts
+++ b/src/providers/openrouter/client.ts
@@ -38,10 +38,10 @@ export class OpenRouterClient {
): Promise {
const url = `${this.baseUrl}${endpoint}`;
- const headers: HeadersInit = {
+ const headers: Record = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
- ...options.headers,
+ ...(options.headers as Record || {}),
};
if (this.httpReferrer) {
@@ -128,6 +128,7 @@ export class OpenRouterClient {
// Check if model supports tools by looking at supported_parameters
const supportsTools = openRouterModel.supported_parameters?.includes('tools') || false;
+ console.log(`Model ${openRouterModel.id} supports tools: ${supportsTools}`);
return {
id: openRouterModel.id,
diff --git a/tsconfig.json b/tsconfig.json
index 714eb81..416e717 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -26,6 +26,6 @@
"@/*": ["./src/*"]
}
},
- "include": ["src", "**/*.ts", "**/*.tsx"],
+ "include": ["src/**/*"],
"references": [{ "path": "./tsconfig.node.json" }]
}
\ No newline at end of file
From 8c46e57b4991d81227d004868d1bc39b73d1c134 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 12 Jun 2025 14:05:01 +0100
Subject: [PATCH 10/33] Implement comprehensive QoL improvements for MCP client
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Features Added
- **Unique server name validation**: Prevent duplicate MCP server names to avoid tool routing conflicts
- **Quick-add server examples**: Added example server buttons for "Everything" and "Test Server" configurations
- **Conversation model selector**: Added dropdown to change AI models directly in chat interface
- **Fixed nav bar height**: Conversation widget now properly accounts for navigation bar height
## User Experience Improvements
- Server name uniqueness prevents tool naming conflicts and confusion
- Quick-add buttons make it easier to get started with common MCP servers
- Model selector allows switching between AI models without leaving conversation
- Proper height calculation eliminates layout issues with conversation interface
## Technical Details
- Server name validation happens before calling addMcpServer API
- Model selector integrates with existing inference context
- Height calculation uses CSS calc(100vh - 4rem) for nav bar compensation
- All changes maintain backward compatibility
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/components/ChatInterface.tsx | 34 ++++++++++++++----
src/components/ConversationApp.tsx | 2 +-
src/components/MCPTest.tsx | 56 +++++++++++++++++++++++++++---
3 files changed, 80 insertions(+), 12 deletions(-)
diff --git a/src/components/ChatInterface.tsx b/src/components/ChatInterface.tsx
index 74b7af5..2117db1 100644
--- a/src/components/ChatInterface.tsx
+++ b/src/components/ChatInterface.tsx
@@ -16,7 +16,12 @@ export function ChatInterface() {
getAgentLoopState,
} = useConversation();
- const { provider: currentProvider } = useInference();
+ const {
+ models,
+ selectedModel,
+ selectModel,
+ isAuthenticated
+ } = useInference();
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef(null);
@@ -29,7 +34,7 @@ export function ChatInterface() {
}, [activeConversation?.messages]);
const handleSendMessage = async (content: string) => {
- if (!activeConversationId || !currentProvider?.isAuthenticated) {
+ if (!activeConversationId || !isAuthenticated) {
return;
}
@@ -65,7 +70,7 @@ export function ChatInterface() {
}
// Show auth prompt if not authenticated
- if (!currentProvider?.isAuthenticated) {
+ if (!isAuthenticated) {
return (
@@ -80,13 +85,28 @@ export function ChatInterface() {
{/* Chat Header */}
-
+
{activeConversation.title}
-
- Model: {currentProvider.selectedModel?.name || 'None selected'}
-
+
+
+ Model:
+
+ selectModel(e.target.value)}
+ disabled={isGenerating || !isAuthenticated}
+ className="text-sm bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded px-2 py-1 text-gray-900 dark:text-gray-100 disabled:opacity-50"
+ >
+ Select a model
+ {models.map((model) => (
+
+ {model.name}
+
+ ))}
+
+
{isGenerating && (
diff --git a/src/components/ConversationApp.tsx b/src/components/ConversationApp.tsx
index 0463e89..7e93758 100644
--- a/src/components/ConversationApp.tsx
+++ b/src/components/ConversationApp.tsx
@@ -11,7 +11,7 @@ export function ConversationApp() {
return (
-
+
{/* Left Sidebar - Conversations */}
diff --git a/src/components/MCPTest.tsx b/src/components/MCPTest.tsx
index aa905a8..a04d943 100644
--- a/src/components/MCPTest.tsx
+++ b/src/components/MCPTest.tsx
@@ -32,8 +32,17 @@ export function MCPTest() {
return;
}
+ const trimmedName = newServerName.trim();
+
+ // Check for duplicate server names
+ const existingServer = connections.find(conn => conn.name === trimmedName);
+ if (existingServer) {
+ alert(`A server with the name "${trimmedName}" already exists. Please choose a different name.`);
+ return;
+ }
+
const config: MCPServerConfig = {
- name: newServerName.trim(),
+ name: trimmedName,
url: newServerUrl.trim(),
transport: 'auto',
authType: 'none',
@@ -48,7 +57,7 @@ export function MCPTest() {
console.error('Failed to add server:', error);
alert(`Failed to add server: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
- }, [newServerName, newServerUrl, addMcpServer]);
+ }, [newServerName, newServerUrl, addMcpServer, connections]);
const handleAddServerWithOAuth = useCallback(async () => {
if (!newServerName.trim() || !newServerUrl.trim()) {
@@ -56,8 +65,17 @@ export function MCPTest() {
return;
}
+ const trimmedName = newServerName.trim();
+
+ // Check for duplicate server names
+ const existingServer = connections.find(conn => conn.name === trimmedName);
+ if (existingServer) {
+ alert(`A server with the name "${trimmedName}" already exists. Please choose a different name.`);
+ return;
+ }
+
const config: MCPServerConfig = {
- name: newServerName.trim(),
+ name: trimmedName,
url: newServerUrl.trim(),
transport: 'auto',
authType: 'oauth',
@@ -72,7 +90,7 @@ export function MCPTest() {
console.error('Failed to add server with OAuth:', error);
alert(`Failed to add server with OAuth: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
- }, [newServerName, newServerUrl, addMcpServer]);
+ }, [newServerName, newServerUrl, addMcpServer, connections]);
const handleRemoveServer = useCallback((connectionId: string) => {
if (confirm('Are you sure you want to remove this server?')) {
@@ -151,6 +169,36 @@ export function MCPTest() {
Add MCP Server
+ {/* Quick Add Examples */}
+
+
+ Quick Add Examples
+
+
+ {
+ setNewServerName('Everything');
+ setNewServerUrl('https://example-server.modelcontextprotocol.io/sse');
+ }}
+ className="px-3 py-1 text-xs bg-blue-600 text-white rounded-md hover:bg-blue-700"
+ >
+ Everything Server
+
+ {
+ setNewServerName('Test Server');
+ setNewServerUrl('https://localhost:3000/mcp');
+ }}
+ className="px-3 py-1 text-xs bg-blue-600 text-white rounded-md hover:bg-blue-700"
+ >
+ Local Test Server
+
+
+
+ Click to auto-fill common server configurations
+
+
+
Date: Thu, 12 Jun 2025 14:23:51 +0100
Subject: [PATCH 11/33] Fix server name normalization for tool routing
compatibility
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Problem
MCP server names with invalid characters (spaces, special chars, etc.)
caused tool routing failures when prefixed to tool names, as OpenRouter
API requires tool names to match pattern ^[a-zA-Z0-9_-]{1,64}$
## Solution
- **Added normalizeServerName utility**: Converts invalid characters to underscores
- **Centralized normalization logic**: Shared utility prevents code duplication
- **Fixed tool routing**: Agent loop now matches normalized server names correctly
- **Updated tool discovery**: MCP tools use normalized prefixes consistently
## Examples
- "My Weather Service\!" → "My_Weather_Service"
- "Test@Server#123" → "Test_Server_123"
- "Server...with...dots" → "Server_with_dots"
- " spaces_and__double__underscores " → "spaces_and_double_underscores"
## Technical Details
- Created /src/utils/mcpUtils.ts for shared normalization function
- Updated MCPConnectionManager to use normalized prefixes in tool names
- Fixed agent loop to match connections using normalized names
- Maintains original server names in UI while using normalized names for API
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/hooks/useAgentLoop.ts | 13 ++++++++-----
src/mcp/connection.ts | 9 ++++++---
src/providers/openrouter/client.ts | 4 ----
src/utils/mcpUtils.ts | 14 ++++++++++++++
4 files changed, 28 insertions(+), 12 deletions(-)
create mode 100644 src/utils/mcpUtils.ts
diff --git a/src/hooks/useAgentLoop.ts b/src/hooks/useAgentLoop.ts
index 63e9e38..61b4ef6 100644
--- a/src/hooks/useAgentLoop.ts
+++ b/src/hooks/useAgentLoop.ts
@@ -15,6 +15,7 @@ import type {
import type { ChatMessage, Tool, ToolCall, InferenceRequest } from '@/types/inference';
import { useInference } from '@/contexts/InferenceContext';
import { useMCP } from '@/contexts/MCPContext';
+import { normalizeServerName } from '@/utils/mcpUtils';
// Test tools that work alongside MCP tools
const testTools: TestTool[] = [
@@ -226,17 +227,19 @@ export function useAgentLoop(config: Partial = {}): UseAgentLoo
// Check if it's an MCP tool (prefixed with server name using double underscore)
if (toolCall.function.name.includes('__')) {
- // Extract server name from tool name (format: "server__tool_name")
- const [serverName] = toolCall.function.name.split('__');
+ // Extract normalized server name from tool name (format: "normalized_server__tool_name")
+ const [normalizedServerName] = toolCall.function.name.split('__');
- // Find the connection ID for this server name
- const connection = connections.find(conn => conn.name === serverName);
+ // Find the connection ID by comparing normalized server names
+ const connection = connections.find(conn => {
+ return normalizeServerName(conn.name) === normalizedServerName;
+ });
if (connection) {
const result = await callMCPTool(connection.id, toolCall.function.name, toolCall.function.arguments);
return { result };
} else {
- throw new Error(`MCP server "${serverName}" not found or not connected`);
+ throw new Error(`MCP server with normalized name "${normalizedServerName}" not found or not connected`);
}
}
diff --git a/src/mcp/connection.ts b/src/mcp/connection.ts
index 92ca4a9..1b13ad0 100644
--- a/src/mcp/connection.ts
+++ b/src/mcp/connection.ts
@@ -17,6 +17,7 @@ import type {
MCPError,
} from '@/types/mcp';
import type { Tool } from '@/types/inference';
+import { normalizeServerName } from '@/utils/mcpUtils';
interface MCPOAuthState {
codeVerifier: string;
@@ -587,10 +588,11 @@ export class MCPConnectionManager {
// Transform MCP tools to our Tool interface with name prefixing
// Use double underscore instead of dot to comply with OpenRouter API requirements
+ const normalizedServerName = normalizeServerName(this.connection.name);
return result.tools.map(tool => ({
type: 'function' as const,
function: {
- name: `${this.connection.name}__${tool.name}`,
+ name: `${normalizedServerName}__${tool.name}`,
description: `[${this.connection.name}] ${tool.description || ''}`,
parameters: tool.inputSchema || {},
},
@@ -643,8 +645,9 @@ export class MCPConnectionManager {
}
// Remove the server prefix from the tool name (using double underscore separator)
- const unprefixedName = toolName.startsWith(`${this.connection.name}__`)
- ? toolName.slice(this.connection.name.length + 2)
+ const normalizedServerName = normalizeServerName(this.connection.name);
+ const unprefixedName = toolName.startsWith(`${normalizedServerName}__`)
+ ? toolName.slice(normalizedServerName.length + 2)
: toolName;
try {
diff --git a/src/providers/openrouter/client.ts b/src/providers/openrouter/client.ts
index f26fc0f..2243cbd 100644
--- a/src/providers/openrouter/client.ts
+++ b/src/providers/openrouter/client.ts
@@ -126,10 +126,6 @@ export class OpenRouterClient {
const inputCost = parseFloat(openRouterModel.pricing.prompt);
const outputCost = parseFloat(openRouterModel.pricing.completion);
- // Check if model supports tools by looking at supported_parameters
- const supportsTools = openRouterModel.supported_parameters?.includes('tools') || false;
- console.log(`Model ${openRouterModel.id} supports tools: ${supportsTools}`);
-
return {
id: openRouterModel.id,
name: openRouterModel.name,
diff --git a/src/utils/mcpUtils.ts b/src/utils/mcpUtils.ts
new file mode 100644
index 0000000..eebc103
--- /dev/null
+++ b/src/utils/mcpUtils.ts
@@ -0,0 +1,14 @@
+// Utility functions for MCP server name normalization
+
+/**
+ * Normalize server name to comply with OpenRouter API tool naming requirements
+ * Pattern: ^[a-zA-Z0-9_-]{1,64}$
+ */
+export function normalizeServerName(name: string): string {
+ return name
+ .replace(/[^a-zA-Z0-9_-]/g, '_') // Replace invalid characters with underscore
+ .replace(/_{2,}/g, '_') // Replace multiple underscores with single
+ .replace(/^_+|_+$/g, '') // Remove leading/trailing underscores
+ .substring(0, 32) // Limit length to leave room for tool name
+ || 'server'; // Fallback if name becomes empty
+}
\ No newline at end of file
From e8197fb2a525f7ac25682d8406316fb8a1c2f050 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 12 Jun 2025 14:26:08 +0100
Subject: [PATCH 12/33] Add persistent model selection with localStorage
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Features
- **Sticky model selection**: Chosen model persists across browser sessions
- **Automatic restoration**: Saved model is restored when loading models
- **Validation**: Invalid saved models are automatically cleared
- **Provider switching**: Model selection is reset when switching providers
## Implementation Details
- Model ID saved to localStorage on selection
- Model restored during loadModels() if still available
- Saved selection cleared when:
- Switching inference providers
- Logging out
- Model no longer exists in available models
## User Experience
- Users no longer need to reselect their preferred model after refresh
- Model selector maintains state across page reloads
- Graceful handling when saved model becomes unavailable
## Technical Notes
- Uses 'selected_model_id' localStorage key
- Integrates with existing InferenceContext state management
- Maintains backward compatibility with existing provider logic
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/contexts/InferenceContext.tsx | 40 ++++++++++++++++++++++++++++---
1 file changed, 37 insertions(+), 3 deletions(-)
diff --git a/src/contexts/InferenceContext.tsx b/src/contexts/InferenceContext.tsx
index 15d2d33..08dea4a 100644
--- a/src/contexts/InferenceContext.tsx
+++ b/src/contexts/InferenceContext.tsx
@@ -42,12 +42,20 @@ export function InferenceContextProvider({ children }: InferenceProviderProps) {
const [provider, setProviderState] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
- const [selectedModelId, setSelectedModelId] = useState(undefined);
+ const [selectedModelId, setSelectedModelId] = useState(() => {
+ // Load saved model selection from localStorage
+ return localStorage.getItem('selected_model_id') || undefined;
+ });
const [_, setAuthStateVersion] = useState(0); // Force re-renders on auth changes
const setProvider = useCallback((newProvider: InferenceProvider) => {
setProviderState(newProvider);
setSelectedModelId(undefined);
+
+ // Clear saved model selection when switching providers
+ // (it will be restored during loadModels if still valid)
+ localStorage.removeItem('selected_model_id');
+
setError(null);
setAuthStateVersion(prev => prev + 1); // Trigger re-render
}, []);
@@ -58,6 +66,10 @@ export function InferenceContextProvider({ children }: InferenceProviderProps) {
}
setProviderState(null);
setSelectedModelId(undefined);
+
+ // Clear saved model selection
+ localStorage.removeItem('selected_model_id');
+
setError(null);
setAuthStateVersion(prev => prev + 1);
}, [provider]);
@@ -115,6 +127,14 @@ export function InferenceContextProvider({ children }: InferenceProviderProps) {
try {
provider.selectModel(modelId);
setSelectedModelId(modelId);
+
+ // Persist model selection to localStorage
+ if (modelId) {
+ localStorage.setItem('selected_model_id', modelId);
+ } else {
+ localStorage.removeItem('selected_model_id');
+ }
+
setError(null);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to select model';
@@ -133,8 +153,22 @@ export function InferenceContextProvider({ children }: InferenceProviderProps) {
try {
const models = await provider.loadModels();
- // Update selectedModelId to match what the provider selected as default
- setSelectedModelId(provider.selectedModel?.id);
+
+ // Try to restore saved model selection
+ const savedModelId = localStorage.getItem('selected_model_id');
+ if (savedModelId && models.find(m => m.id === savedModelId)) {
+ // Saved model exists in the available models, select it
+ provider.selectModel(savedModelId);
+ setSelectedModelId(savedModelId);
+ } else {
+ // Use provider's default selection or clear invalid saved selection
+ if (savedModelId && !models.find(m => m.id === savedModelId)) {
+ // Remove invalid saved model
+ localStorage.removeItem('selected_model_id');
+ }
+ setSelectedModelId(provider.selectedModel?.id);
+ }
+
return models;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to load models';
From d9e63a790e59b4c9ea48976abb8b5de8b20325c5 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 12 Jun 2025 14:38:52 +0100
Subject: [PATCH 13/33] Fix model selector being empty after page refresh
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Problem
After page refresh, the model selector would be empty even though the
authentication provider was restored and models were loaded successfully.
Users had to manually click "reload models" to populate the selector.
## Root Cause
The context state wasn't being properly updated after models were loaded
during provider restoration. The provider had the models, but React
components weren't re-rendering to show them.
## Solution
- **Added forced re-render**: Added extra `setAuthStateVersion()` call after
model loading to trigger React context update
- **Auto-load models on restore**: Provider restoration now automatically
loads models and restores saved model selection
- **Improved error handling**: Better error handling during model restoration
## Technical Details
- Modified `restoreProviderWithModels()` to force context re-render
- Ensured model loading happens immediately when provider is restored
- Maintained existing model persistence functionality
- Cleaned up debugging logs
## User Experience
- ✅ **Seamless model selection**: Model selector is populated immediately after refresh
- ✅ **Persistent choice**: Previously selected model is automatically restored
- ✅ **No manual intervention**: No need to click "reload models" button
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/contexts/InferenceContext.tsx | 37 ++++++++++++++++++++++++++++---
1 file changed, 34 insertions(+), 3 deletions(-)
diff --git a/src/contexts/InferenceContext.tsx b/src/contexts/InferenceContext.tsx
index 08dea4a..48a35a8 100644
--- a/src/contexts/InferenceContext.tsx
+++ b/src/contexts/InferenceContext.tsx
@@ -78,26 +78,56 @@ export function InferenceContextProvider({ children }: InferenceProviderProps) {
setAuthStateVersion(prev => prev + 1);
}, []);
+ // Helper function to restore provider and load models
+ const restoreProviderWithModels = useCallback(async (restoredProvider: InferenceProvider) => {
+ setProviderState(restoredProvider);
+ setAuthStateVersion(prev => prev + 1);
+
+ try {
+ await restoredProvider.loadModels();
+
+ // Restore saved model selection
+ const savedModelId = localStorage.getItem('selected_model_id');
+
+ if (savedModelId && restoredProvider.models.find(m => m.id === savedModelId)) {
+ restoredProvider.selectModel(savedModelId);
+ setSelectedModelId(savedModelId);
+ } else {
+ // Clear invalid saved model
+ if (savedModelId) {
+ localStorage.removeItem('selected_model_id');
+ }
+ setSelectedModelId(restoredProvider.selectedModel?.id);
+ }
+
+ // Force a re-render to update context with loaded models
+ setAuthStateVersion(prev => prev + 1);
+ } catch (error) {
+ console.error('Failed to load models on provider restore:', error);
+ setError(error instanceof Error ? error.message : 'Failed to load models');
+ }
+ }, []);
+
// Auto-restore provider with stored credentials on mount
useEffect(() => {
const tryRestoreProvider = async () => {
// Try API provider first
const apiProvider = new OpenRouterApiProvider();
if (apiProvider.isAuthenticated) {
- setProvider(apiProvider);
+ await restoreProviderWithModels(apiProvider);
return;
}
// Try OAuth provider
const oauthProvider = new OpenRouterOAuthProvider();
if (oauthProvider.isAuthenticated) {
- setProvider(oauthProvider);
+ await restoreProviderWithModels(oauthProvider);
return;
}
};
tryRestoreProvider().catch(console.error);
- }, [setProvider]);
+ }, [restoreProviderWithModels]);
const generateResponse = useCallback(async (request: InferenceRequest): Promise => {
if (!provider) {
@@ -194,6 +224,7 @@ export function InferenceContextProvider({ children }: InferenceProviderProps) {
isAuthenticated: provider?.isAuthenticated || false,
};
+
return (
{children}
From a901e90b597615660ab20a53859fe62e81b70623 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 12 Jun 2025 14:49:53 +0100
Subject: [PATCH 14/33] Fix agent loop UI bleeding between conversations
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Problem
When starting an agent loop in one conversation and switching to another,
the "assistant thinking" UI would incorrectly appear in the second
conversation, even though it was idle.
## Root Cause
The `isLoading` state in ChatInterface was global/shared across all
conversations. When conversation A started generating, `isLoading` was
set to `true`. Switching to conversation B would still show the thinking
UI because `isGenerating = agentLoopState?.isRunning || isLoading`
evaluated to `true` due to the shared loading state.
## Solution
- **Conversation-specific loading**: Changed from single `isLoading` boolean
to `loadingConversations` Set tracking which conversations are loading
- **Isolated state**: Each conversation now has independent loading state
- **Proper cleanup**: Loading state is properly added/removed per conversation
## Technical Changes
- Replaced `useState` with `useState>`
- Updated `handleSendMessage` to add/remove conversation IDs from Set
- Modified `isGenerating` calculation to check current conversation only
## User Experience
- ✅ **Isolated UI state**: Thinking UI only appears for actually running conversations
- ✅ **No cross-conversation interference**: Switching conversations shows correct state
- ✅ **Maintained functionality**: All existing behavior preserved
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/components/ChatInterface.tsx | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/src/components/ChatInterface.tsx b/src/components/ChatInterface.tsx
index 2117db1..cfdc43a 100644
--- a/src/components/ChatInterface.tsx
+++ b/src/components/ChatInterface.tsx
@@ -22,7 +22,7 @@ export function ChatInterface() {
selectModel,
isAuthenticated
} = useInference();
- const [isLoading, setIsLoading] = useState(false);
+ const [loadingConversations, setLoadingConversations] = useState>(new Set());
const messagesEndRef = useRef(null);
const activeConversation = activeConversationId ? getConversation(activeConversationId) : undefined;
@@ -38,13 +38,17 @@ export function ChatInterface() {
return;
}
- setIsLoading(true);
+ setLoadingConversations(prev => new Set(prev).add(activeConversationId));
try {
await sendMessage(activeConversationId, content);
} catch (error) {
console.error('Failed to send message:', error);
} finally {
- setIsLoading(false);
+ setLoadingConversations(prev => {
+ const newSet = new Set(prev);
+ newSet.delete(activeConversationId);
+ return newSet;
+ });
}
};
@@ -78,7 +82,8 @@ export function ChatInterface() {
);
}
- const isGenerating = agentLoopState?.isRunning || isLoading;
+ const isCurrentConversationLoading = activeConversationId ? loadingConversations.has(activeConversationId) : false;
+ const isGenerating = agentLoopState?.isRunning || isCurrentConversationLoading;
return (
From e5a16a1146f499307f39f250a6d3215542e372c3 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 12 Jun 2025 14:53:47 +0100
Subject: [PATCH 15/33] Remove test tabs and simplify UI to conversation-only
interface
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Changes
- **Removed navigation tabs**: Eliminated inference and MCP provider test tabs
- **Simplified App component**: Now directly shows ConversationApp without navigation
- **Full-screen layout**: ConversationApp now uses full viewport height (h-screen)
- **Cleaned up imports**: Removed unused test component imports
- **Deleted test components**: Removed InferenceTest.tsx and MCPTest.tsx files
## Benefits
- ✅ **Streamlined UX**: Single-purpose interface focused on conversations
- ✅ **Smaller bundle**: Reduced JS bundle size by ~25KB and CSS by ~3KB
- ✅ **Cleaner codebase**: Removed complexity of tab navigation and test interfaces
- ✅ **Better focus**: All functionality accessible through conversation interface
## Integration Points Preserved
- MCP server management available through conversation sidebar MCP status
- Model selection integrated into conversation header
- All authentication flows still work through conversation interface
- OAuth callbacks remain functional
The conversation interface now provides access to all functionality:
- Model selection in chat header
- MCP server status/management in sidebar
- Test tools still available through agent loop
- Full authentication and provider management
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/App.tsx | 48 +--
src/components/ConversationApp.tsx | 2 +-
src/components/InferenceTest.tsx | 368 ----------------------
src/components/MCPTest.tsx | 478 -----------------------------
4 files changed, 2 insertions(+), 894 deletions(-)
delete mode 100644 src/components/InferenceTest.tsx
delete mode 100644 src/components/MCPTest.tsx
diff --git a/src/App.tsx b/src/App.tsx
index 0d90bd1..48bc69e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,8 +1,5 @@
-import { useState } from 'react'
import { InferenceContextProvider } from '@/contexts/InferenceContext'
import { MCPProvider } from '@/contexts/MCPContext'
-import { InferenceTest } from '@/components/InferenceTest'
-import { MCPTest } from '@/components/MCPTest'
import { ConversationApp } from '@/components/ConversationApp'
import { OAuthCallback } from '@/components/OAuthCallback'
@@ -20,54 +17,11 @@ function App() {
return ;
}
- const [activeTab, setActiveTab] = useState<'conversations' | 'inference' | 'mcp'>('conversations');
-
return (
- {/* Tab Navigation */}
-
-
-
- setActiveTab('conversations')}
- className={`py-4 px-1 border-b-2 font-medium text-sm ${
- activeTab === 'conversations'
- ? 'border-blue-500 text-blue-600 dark:text-blue-400'
- : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
- }`}
- >
- Conversations
-
- setActiveTab('inference')}
- className={`py-4 px-1 border-b-2 font-medium text-sm ${
- activeTab === 'inference'
- ? 'border-blue-500 text-blue-600 dark:text-blue-400'
- : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
- }`}
- >
- Inference Provider Test
-
- setActiveTab('mcp')}
- className={`py-4 px-1 border-b-2 font-medium text-sm ${
- activeTab === 'mcp'
- ? 'border-blue-500 text-blue-600 dark:text-blue-400'
- : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
- }`}
- >
- MCP Provider Test
-
-
-
-
-
- {/* Tab Content */}
- {activeTab === 'conversations' &&
}
- {activeTab === 'inference' &&
}
- {activeTab === 'mcp' &&
}
+
diff --git a/src/components/ConversationApp.tsx b/src/components/ConversationApp.tsx
index 7e93758..0463e89 100644
--- a/src/components/ConversationApp.tsx
+++ b/src/components/ConversationApp.tsx
@@ -11,7 +11,7 @@ export function ConversationApp() {
return (
-
+
{/* Left Sidebar - Conversations */}
diff --git a/src/components/InferenceTest.tsx b/src/components/InferenceTest.tsx
deleted file mode 100644
index 50d321d..0000000
--- a/src/components/InferenceTest.tsx
+++ /dev/null
@@ -1,368 +0,0 @@
-// Test UI for inference provider functionality
-
-import { useState, useCallback } from 'react';
-import { useInference } from '@/contexts/InferenceContext';
-import { OpenRouterApiProvider, OpenRouterOAuthProvider } from '@/providers/openrouter';
-import type { ChatMessage, InferenceRequest } from '@/types/inference';
-import { testTools, executeTestTool } from '@/utils/testTools';
-
-export function InferenceTest() {
- const {
- provider,
- isLoading,
- error,
- setProvider,
- clearProvider,
- generateResponse,
- selectModel,
- loadModels,
- models,
- selectedModel,
- isAuthenticated,
- } = useInference();
-
- const [apiKey, setApiKey] = useState('');
- const [message, setMessage] = useState('');
- const [response, setResponse] = useState
('');
- const [conversation, setConversation] = useState([]);
- const [enableTools, setEnableTools] = useState(false);
-
- const handleApiAuth = useCallback(async () => {
- if (!apiKey.trim()) {
- alert('Please enter an API key');
- return;
- }
-
- try {
- const apiProvider = new OpenRouterApiProvider();
- await apiProvider.authenticate({ type: 'api_key', apiKey: apiKey.trim() });
- setProvider(apiProvider);
- } catch (err) {
- console.error('API authentication failed:', err);
- alert(`Authentication failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
- }
- }, [apiKey, setProvider]);
-
- const handleOAuthAuth = useCallback(async () => {
- try {
- const oauthProvider = new OpenRouterOAuthProvider();
- await oauthProvider.authenticate({ type: 'oauth' });
- setProvider(oauthProvider);
- } catch (err) {
- console.error('OAuth authentication failed:', err);
- alert(`OAuth failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
- }
- }, [setProvider]);
-
- const handleLoadModels = useCallback(async () => {
- try {
- await loadModels();
- } catch (err) {
- console.error('Failed to load models:', err);
- alert(`Failed to load models: ${err instanceof Error ? err.message : 'Unknown error'}`);
- }
- }, [loadModels]);
-
- const handleSendMessage = useCallback(async () => {
- if (!message.trim() || !provider || !selectedModel) {
- alert('Please enter a message and ensure a model is selected');
- return;
- }
-
- const userMessage: ChatMessage = {
- role: 'user',
- content: message.trim(),
- };
-
- let currentConversation = [...conversation, userMessage];
- setConversation(currentConversation);
- setMessage('');
- setResponse('');
-
- const request: InferenceRequest = {
- messages: currentConversation,
- maxTokens: 500,
- temperature: 0.7,
- tools: enableTools ? testTools : undefined,
- };
-
- try {
- const result = await generateResponse(request);
- let assistantMessage = result.message;
-
- // Handle tool calls
- if (assistantMessage.toolCalls && assistantMessage.toolCalls.length > 0) {
- // Add the assistant message with tool calls
- currentConversation = [...currentConversation, assistantMessage];
- setConversation(currentConversation);
-
- // Execute each tool call and add tool results
- for (const toolCall of assistantMessage.toolCalls) {
- const toolResult = executeTestTool(toolCall.function.name, toolCall.function.arguments);
-
- const toolMessage: ChatMessage = {
- role: 'tool',
- content: toolResult,
- toolCallId: toolCall.id,
- };
-
- currentConversation = [...currentConversation, toolMessage];
- setConversation(currentConversation);
- }
-
- // Make another request with the tool results
- const followUpRequest: InferenceRequest = {
- messages: currentConversation,
- maxTokens: 500,
- temperature: 0.7,
- tools: enableTools ? testTools : undefined,
- };
-
- const followUpResult = await generateResponse(followUpRequest);
- assistantMessage = followUpResult.message;
- setResponse(JSON.stringify(followUpResult, null, 2));
- } else {
- setResponse(JSON.stringify(result, null, 2));
- }
-
- setConversation([...currentConversation, assistantMessage]);
- } catch (err) {
- console.error('Inference failed:', err);
- alert(`Inference failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
- }
- }, [message, provider, selectedModel, conversation, generateResponse, enableTools]);
-
- const handleClearConversation = useCallback(() => {
- setConversation([]);
- setResponse('');
- }, []);
-
- return (
-
-
-
- Inference Provider Test
-
-
- {/* Authentication Section */}
-
-
- Authentication
-
-
- {!isAuthenticated ? (
-
- {/* API Key Auth */}
-
- setApiKey(e.target.value)}
- />
-
- API Key Auth
-
-
-
- {/* OAuth Auth */}
-
-
- OAuth Auth
-
-
-
- ) : (
-
-
- ✓ Authenticated with {provider?.name}
-
-
- Logout
-
-
- )}
-
-
- {/* Model Selection */}
- {isAuthenticated && (
-
-
-
- Model Selection
-
-
- Reload Models
-
-
-
- {models.length > 0 ? (
-
-
selectModel(e.target.value)}
- className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
- >
- Select a model
- {models.map((model) => (
-
- {model.name} ({model.id})
-
- ))}
-
-
- {selectedModel && (
-
- Context: {selectedModel.contextLength} tokens |
- Max output: {selectedModel.capabilities.maxTokens} tokens |
- Tools: ✓ (all models support tools)
-
- )}
-
- ) : (
-
No models loaded
- )}
-
- )}
-
- {/* Chat Interface */}
- {isAuthenticated && selectedModel && (
-
-
-
- Chat Test
-
-
- setEnableTools(e.target.checked)}
- className="rounded"
- />
- Enable Tools
-
-
- Clear
-
-
-
- {/* Tool Test Suggestions */}
- {enableTools && (
-
-
- Tool Test Suggestions:
-
-
-
• "What's the weather in San Francisco?"
-
• "Calculate 123 + 456"
-
• "What time is it in London?"
-
• "Get weather for Tokyo and add 10 + 20"
-
-
- Available tools: get_weather, calculate_sum, get_current_time
-
-
- )}
-
- {/* Conversation */}
- {conversation.length > 0 && (
-
- {conversation.map((msg, index) => (
-
-
- {msg.role === 'user' && '👤 You'}
- {msg.role === 'assistant' && '🤖 Assistant'}
- {msg.role === 'tool' && '🛠️ Tool Result'}
-
-
-
- {typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)}
-
-
- {msg.toolCalls && msg.toolCalls.length > 0 && (
-
- {msg.toolCalls.map((toolCall, tcIndex) => (
-
-
- 🔧 {toolCall.function.name}
-
-
- {JSON.stringify(toolCall.function.arguments, null, 2)}
-
-
- ))}
-
- )}
-
- {msg.toolCallId && (
-
- ↳ Response to tool call: {msg.toolCallId}
-
- )}
-
- ))}
-
- )}
-
- {/* Message Input */}
-
- setMessage(e.target.value)}
- onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
- />
-
- {isLoading ? 'Sending...' : 'Send'}
-
-
-
- )}
-
- {/* Error Display */}
- {error && (
-
- )}
-
- {/* Response Debug */}
- {response && (
-
-
- Last Response (Debug):
-
-
- {response}
-
-
- )}
-
-
- );
-}
\ No newline at end of file
diff --git a/src/components/MCPTest.tsx b/src/components/MCPTest.tsx
deleted file mode 100644
index a04d943..0000000
--- a/src/components/MCPTest.tsx
+++ /dev/null
@@ -1,478 +0,0 @@
-// Test UI for MCP provider functionality
-
-import { useState, useCallback } from 'react';
-import { useMCP } from '@/contexts/MCPContext';
-import type { MCPServerConfig } from '@/types/mcp';
-
-export function MCPTest() {
- const {
- connections,
- isLoading,
- error,
- addMcpServer,
- removeMcpServer,
- reconnectServer,
- getAllTools,
- getToolsForServer,
- callTool,
- getAllResources,
- getConnectedServers,
- } = useMCP();
-
- const [newServerName, setNewServerName] = useState('');
- const [newServerUrl, setNewServerUrl] = useState('');
- const [selectedConnectionId, setSelectedConnectionId] = useState('');
- const [selectedToolName, setSelectedToolName] = useState('');
- const [toolArgs, setToolArgs] = useState('{}');
- const [toolResult, setToolResult] = useState('');
-
- const handleAddServer = useCallback(async () => {
- if (!newServerName.trim() || !newServerUrl.trim()) {
- alert('Please enter both server name and URL');
- return;
- }
-
- const trimmedName = newServerName.trim();
-
- // Check for duplicate server names
- const existingServer = connections.find(conn => conn.name === trimmedName);
- if (existingServer) {
- alert(`A server with the name "${trimmedName}" already exists. Please choose a different name.`);
- return;
- }
-
- const config: MCPServerConfig = {
- name: trimmedName,
- url: newServerUrl.trim(),
- transport: 'auto',
- authType: 'none',
- autoReconnect: false, // Disable auto-reconnect for now to prevent loops
- };
-
- try {
- await addMcpServer(config);
- setNewServerName('');
- setNewServerUrl('');
- } catch (error) {
- console.error('Failed to add server:', error);
- alert(`Failed to add server: ${error instanceof Error ? error.message : 'Unknown error'}`);
- }
- }, [newServerName, newServerUrl, addMcpServer, connections]);
-
- const handleAddServerWithOAuth = useCallback(async () => {
- if (!newServerName.trim() || !newServerUrl.trim()) {
- alert('Please enter both server name and URL');
- return;
- }
-
- const trimmedName = newServerName.trim();
-
- // Check for duplicate server names
- const existingServer = connections.find(conn => conn.name === trimmedName);
- if (existingServer) {
- alert(`A server with the name "${trimmedName}" already exists. Please choose a different name.`);
- return;
- }
-
- const config: MCPServerConfig = {
- name: trimmedName,
- url: newServerUrl.trim(),
- transport: 'auto',
- authType: 'oauth',
- autoReconnect: false,
- };
-
- try {
- await addMcpServer(config);
- setNewServerName('');
- setNewServerUrl('');
- } catch (error) {
- console.error('Failed to add server with OAuth:', error);
- alert(`Failed to add server with OAuth: ${error instanceof Error ? error.message : 'Unknown error'}`);
- }
- }, [newServerName, newServerUrl, addMcpServer, connections]);
-
- const handleRemoveServer = useCallback((connectionId: string) => {
- if (confirm('Are you sure you want to remove this server?')) {
- removeMcpServer(connectionId);
- if (selectedConnectionId === connectionId) {
- setSelectedConnectionId('');
- }
- }
- }, [removeMcpServer, selectedConnectionId]);
-
- const handleReconnectServer = useCallback(async (connectionId: string) => {
- try {
- await reconnectServer(connectionId);
- } catch (error) {
- console.error('Failed to reconnect:', error);
- alert(`Failed to reconnect: ${error instanceof Error ? error.message : 'Unknown error'}`);
- }
- }, [reconnectServer]);
-
- const handleCallTool = useCallback(async () => {
- if (!selectedConnectionId || !selectedToolName) {
- alert('Please select a connection and tool');
- return;
- }
-
- try {
- const args = JSON.parse(toolArgs);
- const result = await callTool(selectedConnectionId, selectedToolName, args);
- setToolResult(JSON.stringify(result, null, 2));
- } catch (error) {
- console.error('Tool call failed:', error);
- const errorResult = {
- error: error instanceof Error ? error.message : 'Unknown error',
- details: error,
- };
- setToolResult(JSON.stringify(errorResult, null, 2));
- }
- }, [selectedConnectionId, selectedToolName, toolArgs, callTool]);
-
- const getStatusColor = (status: string) => {
- switch (status) {
- case 'connected': return 'text-green-600 dark:text-green-400';
- case 'connecting': return 'text-yellow-600 dark:text-yellow-400';
- case 'failed': return 'text-red-600 dark:text-red-400';
- case 'disconnected': return 'text-gray-600 dark:text-gray-400';
- default: return 'text-gray-600 dark:text-gray-400';
- }
- };
-
- const getStatusIcon = (status: string) => {
- switch (status) {
- case 'connected': return '✅';
- case 'connecting': return '🔄';
- case 'failed': return '❌';
- case 'disconnected': return '⚫';
- default: return '❓';
- }
- };
-
- const selectedConnection = connections.find(conn => conn.id === selectedConnectionId);
- const selectedTools = selectedConnection ? getToolsForServer(selectedConnectionId) : [];
- const allTools = getAllTools();
- const allResources = getAllResources();
- const connectedServers = getConnectedServers();
-
- return (
-
-
-
- MCP Provider Test
-
-
- {/* Add Server Section */}
-
-
- Add MCP Server
-
-
- {/* Quick Add Examples */}
-
-
- Quick Add Examples
-
-
- {
- setNewServerName('Everything');
- setNewServerUrl('https://example-server.modelcontextprotocol.io/sse');
- }}
- className="px-3 py-1 text-xs bg-blue-600 text-white rounded-md hover:bg-blue-700"
- >
- Everything Server
-
- {
- setNewServerName('Test Server');
- setNewServerUrl('https://localhost:3000/mcp');
- }}
- className="px-3 py-1 text-xs bg-blue-600 text-white rounded-md hover:bg-blue-700"
- >
- Local Test Server
-
-
-
- Click to auto-fill common server configurations
-
-
-
-
- setNewServerName(e.target.value)}
- />
- setNewServerUrl(e.target.value)}
- />
-
-
-
-
- {isLoading ? 'Adding...' : 'Add Server (No Auth)'}
-
-
- {isLoading ? 'Adding...' : 'Add Server (OAuth)'}
-
-
-
-
- {/* Server List */}
-
-
- Connected Servers ({connections.length})
-
-
- {connections.length === 0 ? (
-
No servers added yet.
- ) : (
-
- {connections.map((connection) => (
-
-
-
-
-
{getStatusIcon(connection.status)}
-
-
- {connection.name}
-
-
- {connection.url}
-
-
-
-
-
-
- Status: {connection.status}
-
-
- Transport: {connection.transport}
-
-
- Tools: {connection.tools.length}
-
-
- Resources: {connection.resources.length}
-
- {connection.connectionAttempts > 0 && (
-
- Attempts: {connection.connectionAttempts}
-
- )}
-
-
- {connection.error && (
-
- Error: {connection.error}
-
- )}
-
- {connection.lastConnected && (
-
- Last connected: {connection.lastConnected.toLocaleString()}
-
- )}
-
-
-
- {connection.status === 'failed' && (
- handleReconnectServer(connection.id)}
- className="px-3 py-1 bg-yellow-600 text-white rounded-md hover:bg-yellow-700 text-sm"
- >
- Retry
-
- )}
- handleRemoveServer(connection.id)}
- className="px-3 py-1 bg-red-600 text-white rounded-md hover:bg-red-700 text-sm"
- >
- Remove
-
-
-
-
- ))}
-
- )}
-
-
- {/* Tool Testing */}
- {connectedServers.length > 0 && (
-
-
- Tool Testing
-
-
-
- {/* Server Selection */}
-
-
- Select Server:
-
- {
- setSelectedConnectionId(e.target.value);
- setSelectedToolName('');
- }}
- className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
- >
- Select a server
- {connectedServers.map((connection) => (
-
- {connection.name} ({connection.tools.length} tools)
-
- ))}
-
-
-
- {/* Tool Selection */}
-
-
- Select Tool:
-
- setSelectedToolName(e.target.value)}
- disabled={!selectedConnectionId}
- className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 disabled:opacity-50"
- >
- Select a tool
- {selectedTools.map((tool) => (
-
- {tool.function.name}
-
- ))}
-
-
-
-
- {/* Tool Arguments */}
-
-
- Tool Arguments (JSON):
-
- setToolArgs(e.target.value)}
- placeholder='{"param1": "value1", "param2": "value2"}'
- className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 font-mono text-sm"
- rows={3}
- />
-
-
- {/* Call Tool Button */}
-
- {isLoading ? 'Calling...' : 'Call Tool'}
-
-
- {/* Tool Result */}
- {toolResult && (
-
-
- Tool Result:
-
-
- {toolResult}
-
-
- )}
-
- )}
-
- {/* Summary Stats */}
-
-
-
- {connections.length}
-
-
- Total Servers
-
-
-
-
-
- {connectedServers.length}
-
-
- Connected
-
-
-
-
-
- {allTools.length}
-
-
- Total Tools
-
-
-
-
-
- {allResources.length}
-
-
- Total Resources
-
-
-
-
- {/* Error Display */}
- {error && (
-
- )}
-
- {/* All Tools List */}
- {allTools.length > 0 && (
-
-
- All Available Tools ({allTools.length})
-
-
- {allTools.map((tool, index) => (
-
-
- {tool.function.name}
-
- {tool.function.description && (
-
- {tool.function.description}
-
- )}
-
- ))}
-
-
- )}
-
-
- );
-}
\ No newline at end of file
From c726e6335347572c9011e3b8a551d9ddcdd309b0 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 12 Jun 2025 14:55:45 +0100
Subject: [PATCH 16/33] Add logout button to conversation sidebar
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Features
- **Logout button**: Added "Log Out" button at bottom of conversation sidebar
- **Confirmation dialog**: Prompts user to confirm before logging out
- **Conditional display**: Only shows when user is authenticated with a provider
- **Proper styling**: Matches sidebar design with subtle gray styling
## Implementation
- Integrated with existing InferenceContext for logout functionality
- Uses `clearProvider()` to handle complete logout process
- Added border separator between conversations and logout button
- Positioned at bottom of sidebar with fixed placement
## User Experience
- ✅ **Easy access**: Logout button always visible when authenticated
- ✅ **Safety confirmation**: Prevents accidental logout with confirm dialog
- ✅ **Clean design**: Subtle styling that doesn't distract from conversations
- ✅ **Complete logout**: Clears all authentication state and persisted data
## Technical Details
- Leverages existing clearProvider() functionality
- Automatically clears API keys, OAuth tokens, and selected models
- Maintains responsive design and dark mode compatibility
- No impact on conversation data (conversations persist locally)
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/components/ConversationSidebar.tsx | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/src/components/ConversationSidebar.tsx b/src/components/ConversationSidebar.tsx
index 094ef33..3568832 100644
--- a/src/components/ConversationSidebar.tsx
+++ b/src/components/ConversationSidebar.tsx
@@ -1,6 +1,7 @@
// Conversation sidebar with conversation list and management
import { useConversation } from '@/contexts/ConversationContext';
+import { useInference } from '@/contexts/InferenceContext';
export function ConversationSidebar() {
const {
@@ -10,11 +11,19 @@ export function ConversationSidebar() {
deleteConversation,
setActiveConversation,
} = useConversation();
+
+ const { clearProvider, provider } = useInference();
const handleNewConversation = () => {
createConversation();
};
+ const handleLogout = () => {
+ if (confirm('Are you sure you want to log out?')) {
+ clearProvider();
+ }
+ };
+
const formatDate = (date: Date): string => {
const now = new Date();
const diff = now.getTime() - date.getTime();
@@ -115,6 +124,18 @@ export function ConversationSidebar() {
)}
+
+ {/* Logout Button */}
+ {provider && (
+
+
+ Log Out
+
+
+ )}
);
}
\ No newline at end of file
From 253db5ebc1d8f0324110339c53a2a8d2cf2547c2 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 12 Jun 2025 15:04:08 +0100
Subject: [PATCH 17/33] Enhance logout to clear MCP OAuth tokens and disconnect
all servers
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Problem
The logout button only cleared inference provider authentication, leaving
MCP OAuth tokens and server connections active. Users remained authenticated
to MCP servers after logging out.
## Solution
- **Clear MCP OAuth tokens**: Remove OAuth tokens and state for all MCP connections
- **Disconnect all servers**: Remove all MCP server connections on logout
- **Complete cleanup**: Comprehensive logout that clears all authentication state
- **Enhanced confirmation**: Updated dialog to inform users about MCP disconnection
## Technical Implementation
- Added MCP context integration to access connections and removal methods
- Clear OAuth tokens from localStorage for each OAuth-enabled connection
- Remove all MCP servers to properly disconnect and clean up connections
- Maintained existing inference provider logout functionality
## User Experience
- ✅ **Complete logout**: All authentication state cleared (inference + MCP)
- ✅ **Clean slate**: No residual authentication data after logout
- ✅ **Clear communication**: User informed about full scope of logout action
- ✅ **Security**: No orphaned authentication tokens left behind
## Data Cleared on Logout
- Inference provider API keys and OAuth tokens
- MCP OAuth tokens and authentication state
- Model selection preferences
- All active MCP server connections
Note: Conversation history remains intact as it's stored separately from auth data.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/components/ConversationSidebar.tsx | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/src/components/ConversationSidebar.tsx b/src/components/ConversationSidebar.tsx
index 3568832..75cb848 100644
--- a/src/components/ConversationSidebar.tsx
+++ b/src/components/ConversationSidebar.tsx
@@ -2,6 +2,7 @@
import { useConversation } from '@/contexts/ConversationContext';
import { useInference } from '@/contexts/InferenceContext';
+import { useMCP } from '@/contexts/MCPContext';
export function ConversationSidebar() {
const {
@@ -13,14 +14,30 @@ export function ConversationSidebar() {
} = useConversation();
const { clearProvider, provider } = useInference();
+ const { connections, removeMcpServer } = useMCP();
const handleNewConversation = () => {
createConversation();
};
const handleLogout = () => {
- if (confirm('Are you sure you want to log out?')) {
+ if (confirm('Are you sure you want to log out? This will disconnect all MCP servers and clear your authentication.')) {
+ // Clear inference provider authentication
clearProvider();
+
+ // Clear MCP OAuth tokens for all connections
+ connections.forEach(connection => {
+ if (connection.authType === 'oauth') {
+ // Clear OAuth tokens and state for this connection
+ localStorage.removeItem(`mcp_oauth_tokens_${connection.id}`);
+ localStorage.removeItem(`mcp_oauth_state_${connection.id}`);
+ }
+ });
+
+ // Remove all MCP servers (this will disconnect them)
+ connections.forEach(connection => {
+ removeMcpServer(connection.id);
+ });
}
};
From 6605fbcab6418154a76d01876c3a82bde4067e96 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 12 Jun 2025 15:11:26 +0100
Subject: [PATCH 18/33] Rename logout to reset and clear all localStorage
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Renamed "Log Out" button to "Reset All Data" to better reflect functionality
- Renamed handleLogout to handleReset for consistency
- Updated confirmation dialog text to say "reset the application"
- Replaced selective localStorage clearing with localStorage.clear() for complete data removal
- Ensures all conversations, authentication, and MCP data is properly cleared
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/components/ConversationSidebar.tsx | 30 ++++++++++++++------------
1 file changed, 16 insertions(+), 14 deletions(-)
diff --git a/src/components/ConversationSidebar.tsx b/src/components/ConversationSidebar.tsx
index 75cb848..c1260db 100644
--- a/src/components/ConversationSidebar.tsx
+++ b/src/components/ConversationSidebar.tsx
@@ -20,24 +20,26 @@ export function ConversationSidebar() {
createConversation();
};
- const handleLogout = () => {
- if (confirm('Are you sure you want to log out? This will disconnect all MCP servers and clear your authentication.')) {
+ const handleReset = () => {
+ if (confirm('Are you sure you want to reset the application? This will clear all data including conversations, authentication, and server connections.')) {
// Clear inference provider authentication
clearProvider();
- // Clear MCP OAuth tokens for all connections
- connections.forEach(connection => {
- if (connection.authType === 'oauth') {
- // Clear OAuth tokens and state for this connection
- localStorage.removeItem(`mcp_oauth_tokens_${connection.id}`);
- localStorage.removeItem(`mcp_oauth_state_${connection.id}`);
- }
- });
-
// Remove all MCP servers (this will disconnect them)
connections.forEach(connection => {
removeMcpServer(connection.id);
});
+
+ // Clear all conversations
+ conversations.forEach(conversation => {
+ deleteConversation(conversation.id);
+ });
+
+ // Clear ALL localStorage data
+ localStorage.clear();
+
+ // Refresh the page to ensure clean state
+ window.location.reload();
}
};
@@ -142,14 +144,14 @@ export function ConversationSidebar() {
)}
- {/* Logout Button */}
+ {/* Reset Button */}
{provider && (
- Log Out
+ Reset All Data
)}
From 1ab5496e0abfdf3ae8c63b946dd980df012ee570 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 12 Jun 2025 15:28:20 +0100
Subject: [PATCH 19/33] Add tabbed sidebar with MCP server management and
resizable interface
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Created tabbed interface with MCP, Conversations, and Inference tabs
- Set MCP tab as default tab
- Added quick connect button for example server with OAuth auth
- Created form to add custom MCP servers with auth type selection
- Implemented server management UI with connect/disconnect/remove actions
- Made sidebar resizable with drag handle (240px-600px range)
- Added comprehensive MCP server status display and tools listing
- Created dedicated Inference tab for OpenRouter connection management
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/components/ConversationApp.tsx | 64 ++++---
src/components/InferenceTab.tsx | 115 +++++++++++++
src/components/MCPTab.tsx | 264 +++++++++++++++++++++++++++++
src/components/TabbedSidebar.tsx | 49 ++++++
src/mcp/connection.ts | 1 +
5 files changed, 469 insertions(+), 24 deletions(-)
create mode 100644 src/components/InferenceTab.tsx
create mode 100644 src/components/MCPTab.tsx
create mode 100644 src/components/TabbedSidebar.tsx
diff --git a/src/components/ConversationApp.tsx b/src/components/ConversationApp.tsx
index 0463e89..bcdd268 100644
--- a/src/components/ConversationApp.tsx
+++ b/src/components/ConversationApp.tsx
@@ -1,38 +1,54 @@
// Main conversation application with sidebar and chat interface
-import { useState } from 'react';
+import { useState, useRef, useCallback } from 'react';
import { ConversationProvider } from '@/contexts/ConversationContext';
-import { ConversationSidebar } from './ConversationSidebar';
+import { TabbedSidebar } from './TabbedSidebar';
import { ChatInterface } from './ChatInterface';
-import { MCPStatus } from './MCPStatus';
export function ConversationApp() {
- const [showMCPStatus, setShowMCPStatus] = useState(false);
+ const [sidebarWidth, setSidebarWidth] = useState(320);
+ const isResizing = useRef(false);
+
+ const handleMouseDown = useCallback(() => {
+ isResizing.current = true;
+ document.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseup', handleMouseUp);
+ document.body.style.cursor = 'col-resize';
+ document.body.style.userSelect = 'none';
+ }, []);
+
+ const handleMouseMove = useCallback((e: MouseEvent) => {
+ if (!isResizing.current) return;
+
+ const newWidth = e.clientX;
+ if (newWidth >= 240 && newWidth <= 600) {
+ setSidebarWidth(newWidth);
+ }
+ }, []);
+
+ const handleMouseUp = useCallback(() => {
+ isResizing.current = false;
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ document.body.style.cursor = '';
+ document.body.style.userSelect = '';
+ }, [handleMouseMove]);
return (
- {/* Left Sidebar - Conversations */}
-
-
-
-
- Conversations
-
- setShowMCPStatus(!showMCPStatus)}
- className="text-sm px-3 py-1 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"
- >
- {showMCPStatus ? 'Hide' : 'Show'} MCP
-
-
-
+ {/* Left Sidebar - Tabbed Interface */}
+
+
- {showMCPStatus ? (
-
- ) : (
-
- )}
+ {/* Resize Handle */}
+
{/* Main Chat Area */}
diff --git a/src/components/InferenceTab.tsx b/src/components/InferenceTab.tsx
new file mode 100644
index 0000000..df8f9ae
--- /dev/null
+++ b/src/components/InferenceTab.tsx
@@ -0,0 +1,115 @@
+// Inference tab for managing OpenRouter connection
+
+import { useState } from 'react';
+import { useInference } from '@/contexts/InferenceContext';
+import { OpenRouterOAuthProvider } from '@/providers/openrouter/oauth-provider';
+
+export function InferenceTab() {
+ const { provider, models, clearProvider, setProvider } = useInference();
+ const [isConnecting, setIsConnecting] = useState(false);
+
+ const handleConnect = async () => {
+ setIsConnecting(true);
+ try {
+ const oauthProvider = new OpenRouterOAuthProvider();
+ await oauthProvider.authenticate({ type: 'oauth' });
+ setProvider(oauthProvider);
+ } catch (error) {
+ console.error('Failed to connect to OpenRouter:', error);
+ } finally {
+ setIsConnecting(false);
+ }
+ };
+
+ const handleDisconnect = () => {
+ if (confirm('Are you sure you want to disconnect from OpenRouter? This will clear your authentication.')) {
+ clearProvider();
+ }
+ };
+
+ return (
+
+
+ {/* Connection Status */}
+
+
+ Inference Provider
+
+
+ {provider ? (
+
+
+
+
+ 🟢 Connected to OpenRouter
+
+
+ {models.length} models available
+
+
+
+ Disconnect
+
+
+
+ ) : (
+
+
+ ⚪ Not connected
+
+
+ {isConnecting ? 'Connecting...' : 'Connect to OpenRouter'}
+
+
+ )}
+
+
+ {/* Available Models */}
+ {provider && models.length > 0 && (
+
+
+ Available Models ({models.length})
+
+
+ {models.map((model) => (
+
+
+ {model.id}
+
+ {model.name && model.name !== model.id && (
+
+ {model.name}
+
+ )}
+
+ Context: {model.contextLength?.toLocaleString() || 'Unknown'}
+
+
+ ))}
+
+
+ )}
+
+ {/* Help Text */}
+
+
+ Connect to OpenRouter to access AI models for conversations.
+
+
+ You'll need an OpenRouter account and API key to authenticate.
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/MCPTab.tsx b/src/components/MCPTab.tsx
new file mode 100644
index 0000000..febfc2a
--- /dev/null
+++ b/src/components/MCPTab.tsx
@@ -0,0 +1,264 @@
+// MCP tab with server management and connection functionality
+
+import { useState } from 'react';
+import { useMCP } from '@/contexts/MCPContext';
+
+export function MCPTab() {
+ const { connections, addMcpServer, removeMcpServer, reconnectServer, isLoading } = useMCP();
+ const [showAddForm, setShowAddForm] = useState(false);
+ const [newServerName, setNewServerName] = useState('');
+ const [newServerUrl, setNewServerUrl] = useState('');
+ const [authType, setAuthType] = useState<'none' | 'oauth'>('none');
+
+ const handleAddExampleServer = async () => {
+ try {
+ await addMcpServer({
+ name: 'Example Server',
+ url: 'https://example-server.modelcontextprotocol.io/sse',
+ authType: 'oauth',
+ });
+ } catch (error) {
+ console.error('Failed to add example server:', error);
+ }
+ };
+
+ const handleAddCustomServer = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!newServerName.trim() || !newServerUrl.trim()) return;
+
+ try {
+ await addMcpServer({
+ name: newServerName,
+ url: newServerUrl,
+ authType,
+ });
+
+ // Reset form
+ setNewServerName('');
+ setNewServerUrl('');
+ setAuthType('none');
+ setShowAddForm(false);
+ } catch (error) {
+ console.error('Failed to add custom server:', error);
+ }
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'connected':
+ return 'text-green-600 dark:text-green-400';
+ case 'connecting':
+ return 'text-yellow-600 dark:text-yellow-400';
+ case 'failed':
+ return 'text-red-600 dark:text-red-400';
+ default:
+ return 'text-gray-600 dark:text-gray-400';
+ }
+ };
+
+ const getStatusIcon = (status: string) => {
+ switch (status) {
+ case 'connected':
+ return '🟢';
+ case 'connecting':
+ return '🟡';
+ case 'failed':
+ return '🔴';
+ default:
+ return '⚪';
+ }
+ };
+
+ const connectedServers = connections.filter(conn => conn.status === 'connected');
+ const allTools = connections.flatMap(conn => conn.tools);
+
+ return (
+
+ {/* Summary */}
+
+
+
+ MCP Summary
+
+
+
+ {connectedServers.length} servers connected
+
+
+ {allTools.length} tools available
+
+
+
+
+
+ {/* Add Servers Section */}
+
+
+ Add Servers
+
+
+ {/* Example Server Button */}
+
conn.name === 'Example Server')}
+ className="w-full mb-3 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
+ >
+ {connections.some(conn => conn.name === 'Example Server') ? 'Example Server Added' : 'Add Example Server'}
+
+
+ {/* Custom Server Form Toggle */}
+
setShowAddForm(!showAddForm)}
+ className="w-full px-4 py-2 text-sm bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
+ >
+ {showAddForm ? 'Cancel' : 'Add Custom Server'}
+
+
+ {/* Custom Server Form */}
+ {showAddForm && (
+
+
+
+ Server Name
+
+ setNewServerName(e.target.value)}
+ placeholder="My MCP Server"
+ className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+ required
+ />
+
+
+
+
+ Server URL
+
+ setNewServerUrl(e.target.value)}
+ placeholder="https://your-server.com/sse"
+ className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+ required
+ />
+
+
+
+
+ Authentication
+
+ setAuthType(e.target.value as 'none' | 'oauth')}
+ className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+ >
+ None
+ OAuth
+
+
+
+
+ {isLoading ? 'Adding...' : 'Add Server'}
+
+
+ )}
+
+
+ {/* Server List */}
+
+
+
+ Servers ({connections.length})
+
+
+
+ {connections.length === 0 ? (
+
+ No MCP servers configured. Add a server above to get started.
+
+ ) : (
+
+ {connections.map((connection) => (
+
+
+
+
+
+ {getStatusIcon(connection.status)}
+
+
+ {connection.name}
+
+
+
+ {connection.status}
+
+ {connection.error && (
+
+ {connection.error}
+
+ )}
+
+ {connection.url}
+
+
+
+
+ {(connection.status === 'failed' || connection.status === 'disconnected') && (
+ reconnectServer(connection.id)}
+ className="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200 rounded hover:bg-blue-200 dark:hover:bg-blue-800"
+ title="Reconnect server"
+ >
+ Reconnect
+
+ )}
+ removeMcpServer(connection.id)}
+ className="px-2 py-1 text-xs bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-200 rounded hover:bg-red-200 dark:hover:bg-red-800"
+ title="Remove server"
+ >
+ Remove
+
+
+
+
+ {connection.tools.length > 0 && (
+
+
+ Tools ({connection.tools.length}):
+
+
+ {connection.tools.slice(0, 3).map((tool, index) => (
+
+ {tool.function.name.split('__').pop()}
+
+ ))}
+ {connection.tools.length > 3 && (
+
+ +{connection.tools.length - 3} more
+
+ )}
+
+
+ )}
+
+ ))}
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/TabbedSidebar.tsx b/src/components/TabbedSidebar.tsx
new file mode 100644
index 0000000..a827a5f
--- /dev/null
+++ b/src/components/TabbedSidebar.tsx
@@ -0,0 +1,49 @@
+// Tabbed sidebar with Conversations, MCP, and Inference tabs
+
+import { useState } from 'react';
+import { ConversationSidebar } from './ConversationSidebar';
+import { MCPTab } from './MCPTab';
+import { InferenceTab } from './InferenceTab';
+
+type TabType = 'mcp' | 'conversations' | 'inference';
+
+export function TabbedSidebar() {
+ const [activeTab, setActiveTab] = useState
('mcp');
+
+ const tabs = [
+ { id: 'mcp' as const, label: 'MCP', icon: '🔧' },
+ { id: 'conversations' as const, label: 'Conversations', icon: '💬' },
+ { id: 'inference' as const, label: 'Inference', icon: '🤖' },
+ ];
+
+ return (
+
+ {/* Tab Headers */}
+
+
+ {tabs.map((tab) => (
+ setActiveTab(tab.id)}
+ className={`flex-1 px-4 py-3 text-sm font-medium transition-colors ${
+ activeTab === tab.id
+ ? 'text-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400 bg-blue-50 dark:bg-blue-900/20'
+ : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50'
+ }`}
+ >
+ {tab.icon}
+ {tab.label}
+
+ ))}
+
+
+
+ {/* Tab Content */}
+
+ {activeTab === 'mcp' && }
+ {activeTab === 'conversations' && }
+ {activeTab === 'inference' && }
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/mcp/connection.ts b/src/mcp/connection.ts
index 1b13ad0..9b69a5c 100644
--- a/src/mcp/connection.ts
+++ b/src/mcp/connection.ts
@@ -522,6 +522,7 @@ export class MCPConnectionManager {
try {
console.log('Initializing MCP client...');
this.transport = transport;
+ this.transport.onmessage = console.log.bind(console, 'MCP Client message received:');
this.client = new Client(
{
name: 'example-remote-client',
From 91608a0b97f814417a032c6a94ee714f310861b0 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Fri, 13 Jun 2025 19:07:01 +0100
Subject: [PATCH 20/33] Fix MCP message monitor layout and add collapsible
sections
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Made MCP tab sections (Summary, Add Servers, Server List) collapsible to save space
- Expanded sidebar width limit from 600px to 80% of window width for better debugging
- Fixed message monitor overflow by establishing proper height constraint chain
- Removed auto-scroll and show/hide toggle for message monitor - always visible now
- Server list now has fixed height (max-h-60) with internal scroll
- Message monitor takes remaining space with proper internal scrolling
- Used h-full chain instead of flex-1 to establish explicit height constraints
- Message monitor now properly scrolls within its container instead of at app level
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/components/ConversationApp.tsx | 4 +-
src/components/MCPMessageMonitor.tsx | 161 +++++++++++++++++++++++++++
src/components/MCPTab.tsx | 128 ++++++++++++++-------
src/components/TabbedSidebar.tsx | 6 +-
src/contexts/MCPContext.tsx | 51 +++++++++
src/mcp/connection.ts | 81 +++++---------
src/mcp/debugTransport.ts | 63 +++++++++++
src/types/mcp.ts | 16 +++
8 files changed, 409 insertions(+), 101 deletions(-)
create mode 100644 src/components/MCPMessageMonitor.tsx
create mode 100644 src/mcp/debugTransport.ts
diff --git a/src/components/ConversationApp.tsx b/src/components/ConversationApp.tsx
index bcdd268..c1faafc 100644
--- a/src/components/ConversationApp.tsx
+++ b/src/components/ConversationApp.tsx
@@ -21,7 +21,7 @@ export function ConversationApp() {
if (!isResizing.current) return;
const newWidth = e.clientX;
- if (newWidth >= 240 && newWidth <= 600) {
+ if (newWidth >= 240 && newWidth <= window.innerWidth * 0.8) {
setSidebarWidth(newWidth);
}
}, []);
@@ -39,7 +39,7 @@ export function ConversationApp() {
{/* Left Sidebar - Tabbed Interface */}
diff --git a/src/components/MCPMessageMonitor.tsx b/src/components/MCPMessageMonitor.tsx
new file mode 100644
index 0000000..8aefdc3
--- /dev/null
+++ b/src/components/MCPMessageMonitor.tsx
@@ -0,0 +1,161 @@
+// Component to monitor and display MCP messages
+
+import { useState, useEffect, useRef } from 'react';
+import { useMCP } from '@/contexts/MCPContext';
+import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
+
+interface MCPMessageEntry {
+ id: string;
+ timestamp: Date;
+ connectionId: string;
+ connectionName: string;
+ direction: 'sent' | 'received';
+ message: JSONRPCMessage;
+ extra?: any;
+}
+
+export function MCPMessageMonitor() {
+ const { addMessageCallback, removeMessageCallback, connections } = useMCP();
+ const [messages, setMessages] = useState
([]);
+ const [maxMessages, setMaxMessages] = useState(50);
+ const messagesEndRef = useRef(null);
+
+ // Remove auto-scroll - let user control scroll position
+
+ // Register message callback
+ useEffect(() => {
+ const callbackId = addMessageCallback((connectionId, _client, message, direction, extra) => {
+ const connection = connections.find(c => c.id === connectionId);
+
+ setMessages(prev => {
+ const newMessage: MCPMessageEntry = {
+ id: `${Date.now()}-${Math.random()}`,
+ timestamp: new Date(),
+ connectionId,
+ connectionName: connection?.name || 'Unknown',
+ direction,
+ message,
+ extra,
+ };
+
+ // Keep only the latest maxMessages
+ const newMessages = [...prev, newMessage];
+ return newMessages.slice(-maxMessages);
+ });
+ });
+
+ return () => {
+ removeMessageCallback(callbackId);
+ };
+ }, [addMessageCallback, removeMessageCallback, connections, maxMessages]);
+
+ const formatMessage = (msg: JSONRPCMessage) => {
+ if ('method' in msg) {
+ return `${msg.method}${'id' in msg ? ` (${msg.id})` : ''}`;
+ }
+ if ('id' in msg && 'result' in msg) {
+ return `Response (${msg.id})`;
+ }
+ if ('id' in msg && 'error' in msg) {
+ return `Error (${msg.id})`;
+ }
+ return 'Unknown';
+ };
+
+ const getMessageTypeColor = (msg: JSONRPCMessage, direction: string) => {
+ const baseClass = direction === 'sent'
+ ? 'bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-800'
+ : 'bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-800';
+
+ if ('error' in msg) {
+ return 'bg-red-50 dark:bg-red-900/30 border-red-200 dark:border-red-800';
+ }
+
+ return baseClass;
+ };
+
+ const clearMessages = () => {
+ setMessages([]);
+ };
+
+ return (
+
+
+
+ Message Monitor ({messages.length})
+
+
+ Clear
+
+
+
+
+ {messages.length === 0 ? (
+
+ No messages yet. Messages will appear here as they're sent/received.
+
+ ) : (
+
+ {messages.map((entry) => (
+
+
+
+
+ {entry.direction === 'sent' ? '→' : '←'}
+
+
+ {formatMessage(entry.message)}
+
+
+ {entry.connectionName}
+
+
+
+ {entry.timestamp.toLocaleTimeString()}
+
+
+
+
+ Details
+
+
+ {JSON.stringify(entry.message, null, 2)}
+
+
+
+ ))}
+
+
+ )}
+
+
+ {/* Controls */}
+
+
+
+ Max messages:
+
+ setMaxMessages(Number(e.target.value))}
+ className="px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ >
+ 25
+ 50
+ 100
+ 200
+
+
+
+ Live monitoring active
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/MCPTab.tsx b/src/components/MCPTab.tsx
index febfc2a..c77f497 100644
--- a/src/components/MCPTab.tsx
+++ b/src/components/MCPTab.tsx
@@ -2,6 +2,7 @@
import { useState } from 'react';
import { useMCP } from '@/contexts/MCPContext';
+import { MCPMessageMonitor } from './MCPMessageMonitor';
export function MCPTab() {
const { connections, addMcpServer, removeMcpServer, reconnectServer, isLoading } = useMCP();
@@ -9,6 +10,11 @@ export function MCPTab() {
const [newServerName, setNewServerName] = useState('');
const [newServerUrl, setNewServerUrl] = useState('');
const [authType, setAuthType] = useState<'none' | 'oauth'>('none');
+
+ // Collapsible section states
+ const [showSummary, setShowSummary] = useState(true);
+ const [showAddServers, setShowAddServers] = useState(true);
+ const [showServerList, setShowServerList] = useState(true);
const handleAddExampleServer = async () => {
try {
@@ -73,50 +79,75 @@ export function MCPTab() {
const allTools = connections.flatMap(conn => conn.tools);
return (
-
+
{/* Summary */}
-
-
-
- MCP Summary
-
-
-
- {connectedServers.length} servers connected
-
-
- {allTools.length} tools available
-
+
+
setShowSummary(!showSummary)}
+ className="w-full p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
+ >
+
+
+ MCP Summary
+
+
+ {showSummary ? '−' : '+'}
+
-
+
+ {showSummary && (
+
+
+
+
+ {connectedServers.length} servers connected
+
+
+ {allTools.length} tools available
+
+
+
+
+ )}
{/* Add Servers Section */}
-
-
- Add Servers
-
-
- {/* Example Server Button */}
+
conn.name === 'Example Server')}
- className="w-full mb-3 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
+ onClick={() => setShowAddServers(!showAddServers)}
+ className="w-full p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
>
- {connections.some(conn => conn.name === 'Example Server') ? 'Example Server Added' : 'Add Example Server'}
+
+
+ Add Servers
+
+
+ {showAddServers ? '−' : '+'}
+
+
+ {showAddServers && (
+
+ {/* Example Server Button */}
+
conn.name === 'Example Server')}
+ className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
+ >
+ {connections.some(conn => conn.name === 'Example Server') ? 'Example Server Added' : 'Add Example Server'}
+
- {/* Custom Server Form Toggle */}
-
setShowAddForm(!showAddForm)}
- className="w-full px-4 py-2 text-sm bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
- >
- {showAddForm ? 'Cancel' : 'Add Custom Server'}
-
+ {/* Custom Server Form Toggle */}
+
setShowAddForm(!showAddForm)}
+ className="w-full px-4 py-2 text-sm bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
+ >
+ {showAddForm ? 'Cancel' : 'Add Custom Server'}
+
- {/* Custom Server Form */}
- {showAddForm && (
-
+ {/* Custom Server Form */}
+ {showAddForm && (
+
Server Name
@@ -168,15 +199,27 @@ export function MCPTab() {
)}
+
+ )}
{/* Server List */}
-
-
-
- Servers ({connections.length})
-
-
+
+
setShowServerList(!showServerList)}
+ className="w-full p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors border-b border-gray-200 dark:border-gray-700"
+ >
+
+
+ Servers ({connections.length})
+
+
+ {showServerList ? '−' : '+'}
+
+
+
+ {showServerList && (
+
{connections.length === 0 ? (
@@ -258,7 +301,12 @@ export function MCPTab() {
))}
)}
+
+ )}
+
+ {/* Message Monitor */}
+
);
}
\ No newline at end of file
diff --git a/src/components/TabbedSidebar.tsx b/src/components/TabbedSidebar.tsx
index a827a5f..89565ae 100644
--- a/src/components/TabbedSidebar.tsx
+++ b/src/components/TabbedSidebar.tsx
@@ -17,9 +17,9 @@ export function TabbedSidebar() {
];
return (
-
+
{/* Tab Headers */}
-
+
{tabs.map((tab) => (
{/* Tab Content */}
-
+
{activeTab === 'mcp' &&
}
{activeTab === 'conversations' &&
}
{activeTab === 'inference' &&
}
diff --git a/src/contexts/MCPContext.tsx b/src/contexts/MCPContext.tsx
index 71bc612..4c3e42b 100644
--- a/src/contexts/MCPContext.tsx
+++ b/src/contexts/MCPContext.tsx
@@ -8,6 +8,7 @@ import type {
MCPServerConfig,
MCPResource,
MCPContextValue,
+ MCPMessageCallback,
} from '@/types/mcp';
import type { Tool } from '@/types/inference';
import { MCPConnectionManager } from '@/mcp/connection';
@@ -24,6 +25,33 @@ export function MCPProvider({ children }: MCPProviderProps) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState
(null);
const hasLoadedPersisted = useRef(false);
+
+ // Message callback management
+ const [messageCallbacks, setMessageCallbacks] = useState>(new Map());
+ const messageCallbacksRef = useRef>(new Map());
+
+ // Keep ref in sync with state
+ useEffect(() => {
+ messageCallbacksRef.current = messageCallbacks;
+ }, [messageCallbacks]);
+
+ // Function to broadcast messages to all callbacks (stable reference)
+ const broadcastMessage = useCallback((
+ connectionId: string,
+ client: any,
+ message: any,
+ direction: 'sent' | 'received',
+ extra?: any
+ ) => {
+ console.log(`MCP Message [${direction}] from ${connectionId}:`, message); // Debug log
+ messageCallbacksRef.current.forEach(callback => {
+ try {
+ callback(connectionId, client, message, direction, extra);
+ } catch (error) {
+ console.error('Error in MCP message callback:', error);
+ }
+ });
+ }, []); // No dependencies - uses ref
// Load persisted connections from localStorage on mount
useEffect(() => {
@@ -61,6 +89,9 @@ export function MCPProvider({ children }: MCPProviderProps) {
);
});
+ // Set up message callback
+ manager.setMessageCallback(broadcastMessage);
+
// Add to managers map
setManagers(prev => new Map(prev).set(connectionId, manager));
@@ -127,6 +158,9 @@ export function MCPProvider({ children }: MCPProviderProps) {
);
});
+ // Set up message callback
+ manager.setMessageCallback(broadcastMessage);
+
// Add to managers map
setManagers(prev => new Map(prev).set(connectionId, manager));
@@ -336,6 +370,21 @@ export function MCPProvider({ children }: MCPProviderProps) {
}
}, [managers]);
+ // Message callback management
+ const addMessageCallback = useCallback((callback: MCPMessageCallback): string => {
+ const callbackId = uuidv4();
+ setMessageCallbacks(prev => new Map(prev).set(callbackId, callback));
+ return callbackId;
+ }, []);
+
+ const removeMessageCallback = useCallback((callbackId: string) => {
+ setMessageCallbacks(prev => {
+ const newMap = new Map(prev);
+ newMap.delete(callbackId);
+ return newMap;
+ });
+ }, []);
+
const contextValue: MCPContextValue = {
connections,
isLoading,
@@ -353,6 +402,8 @@ export function MCPProvider({ children }: MCPProviderProps) {
getConnectionById,
updateServerConfig,
handleOAuthCallback,
+ addMessageCallback,
+ removeMessageCallback,
};
return (
diff --git a/src/mcp/connection.ts b/src/mcp/connection.ts
index 9b69a5c..1c25379 100644
--- a/src/mcp/connection.ts
+++ b/src/mcp/connection.ts
@@ -18,6 +18,7 @@ import type {
} from '@/types/mcp';
import type { Tool } from '@/types/inference';
import { normalizeServerName } from '@/utils/mcpUtils';
+import { DebugTransport } from './debugTransport';
interface MCPOAuthState {
codeVerifier: string;
@@ -107,7 +108,6 @@ class MCPOAuthProvider implements OAuthClientProvider {
}
async saveClientInformation(clientInformation: OAuthClientInformation): Promise {
- console.log('Registered OAuth client for MCP server');
const serverKey = this.getServerKey();
localStorage.setItem(`mcp_oauth_client_${serverKey}`, JSON.stringify(clientInformation));
}
@@ -118,12 +118,10 @@ class MCPOAuthProvider implements OAuthClientProvider {
}
async saveTokens(tokens: OAuthTokens): Promise {
- console.log('OAuth tokens saved successfully');
localStorage.setItem(`mcp_oauth_tokens_${this.connectionId}`, JSON.stringify(tokens));
}
async redirectToAuthorization(authorizationUrl: URL): Promise {
- console.log('🔐 Starting OAuth flow in popup window...');
// Open popup for OAuth flow
const popup = window.open(
@@ -149,13 +147,10 @@ class MCPOAuthProvider implements OAuthClientProvider {
popup.close();
if (event.data.error) {
- console.error('❌ OAuth authorization failed:', event.data.error);
this.authError = event.data.error;
} else if (event.data.code) {
- console.log('✅ OAuth authorization successful, exchanging code for tokens...');
this.processAuthorizationCode(event.data.code);
} else {
- console.error('❌ OAuth callback missing authorization code');
this.authError = 'No authorization code received';
}
};
@@ -206,7 +201,6 @@ class MCPOAuthProvider implements OAuthClientProvider {
});
if (result === 'AUTHORIZED') {
- console.log('🎉 OAuth authentication completed! Connecting to MCP server...');
this.notifyOAuthComplete();
} else {
console.error('❌ OAuth token exchange failed');
@@ -241,6 +235,7 @@ export class MCPConnectionManager {
private healthCheckInterval?: NodeJS.Timeout;
private oauthProvider?: MCPOAuthProvider;
private onConnectionUpdate?: () => void;
+ private onMessage?: (connectionId: string, client: any, message: any, direction: 'sent' | 'received', extra?: any) => void;
constructor(id: string, config: MCPServerConfig) {
this.connection = {
@@ -275,6 +270,12 @@ export class MCPConnectionManager {
this.onConnectionUpdate = callback;
}
+ // Set callback for message monitoring
+ setMessageCallback(callback: (connectionId: string, client: any, message: any, direction: 'sent' | 'received', extra?: any) => void): void {
+ console.log(`Setting message callback for connection ${this.connection.id}`); // Debug log
+ this.onMessage = callback;
+ }
+
// Notify about connection state changes
private notifyConnectionUpdate(): void {
if (this.onConnectionUpdate) {
@@ -287,19 +288,6 @@ export class MCPConnectionManager {
this.connection.error = undefined;
try {
- // Validate URL format
- console.log('Validating URL:', this.connection.url);
- try {
- const url = new URL(this.connection.url);
- console.log('URL parsed successfully:', {
- protocol: url.protocol,
- hostname: url.hostname,
- pathname: url.pathname,
- port: url.port
- });
- } catch (urlError) {
- throw new Error(`Invalid URL format: ${this.connection.url}`);
- }
// Clear any existing connections
await this.disconnect();
@@ -343,7 +331,6 @@ export class MCPConnectionManager {
if (this.connection.config.autoReconnect !== false &&
this.connection.connectionAttempts < (this.connection.config.maxReconnectAttempts || 5)) {
const delay = this.getBackoffDelay();
- console.log(`Scheduling reconnect attempt ${this.connection.connectionAttempts} in ${delay}ms`);
this.reconnectTimeout = setTimeout(async () => {
try {
await this.reconnect();
@@ -397,14 +384,11 @@ export class MCPConnectionManager {
throw new Error('OAuth provider not initialized');
}
- console.log('Processing OAuth callback with authorization code...');
try {
// If we have an active transport, use its finishAuth method
if (this.transport && typeof (this.transport as any).finishAuth === 'function') {
- console.log('Calling transport.finishAuth...');
await (this.transport as any).finishAuth(authorizationCode);
- console.log('OAuth authorization completed via transport');
// Now attempt to connect - the transport should be authenticated
await this.connect();
@@ -412,11 +396,9 @@ export class MCPConnectionManager {
// If no transport yet, store the authorization code and try connecting
// The transport creation will handle the auth flow
this.oauthProvider.pendingAuthorizationCode = authorizationCode;
- console.log('Stored authorization code, attempting connection...');
await this.connect();
}
- console.log('OAuth callback processed successfully');
} catch (error) {
console.error('OAuth callback processing failed:', error);
throw new Error(`OAuth callback failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -450,7 +432,6 @@ export class MCPConnectionManager {
// Handle successful OAuth completion
private async handleOAuthSuccess(): Promise {
- console.log('OAuth completed successfully, retrying connection...');
try {
// Reset connection state and retry
@@ -461,7 +442,6 @@ export class MCPConnectionManager {
// Attempt to connect now that we have valid tokens
await this.connect();
- console.log('Post-OAuth connection successful!');
this.notifyConnectionUpdate();
} catch (error) {
console.error('Post-OAuth connection failed:', error);
@@ -474,7 +454,6 @@ export class MCPConnectionManager {
private async tryStreamableHttp(): Promise {
try {
- console.log('Attempting StreamableHTTP connection to:', this.connection.url);
// Create transport options with OAuth provider if configured
const transportOptions: any = {};
@@ -485,7 +464,6 @@ export class MCPConnectionManager {
const transport = new StreamableHTTPClientTransport(new URL(this.connection.url), transportOptions);
await this.initializeClient(transport);
- console.log('StreamableHTTP connection successful');
} catch (error) {
console.error('StreamableHTTP connection failed:', error);
throw error;
@@ -494,7 +472,6 @@ export class MCPConnectionManager {
private async trySSE(): Promise {
try {
- console.log('Attempting SSE connection to:', this.connection.url);
// Create transport options with OAuth provider if configured
const transportOptions: any = {};
@@ -504,25 +481,17 @@ export class MCPConnectionManager {
}
const transport = new SSEClientTransport(new URL(this.connection.url), transportOptions);
- console.log('SSE transport created, attempting client connection...');
await this.initializeClient(transport);
- console.log('SSE connection successful');
} catch (error) {
- console.error('SSE connection failed:', error);
- console.error('Error details:', {
- message: error instanceof Error ? error.message : 'Unknown error',
- stack: error instanceof Error ? error.stack : undefined,
- url: this.connection.url
- });
throw error;
}
}
private async initializeClient(transport: Transport): Promise {
try {
- console.log('Initializing MCP client...');
- this.transport = transport;
- this.transport.onmessage = console.log.bind(console, 'MCP Client message received:');
+ const debugTransport = new DebugTransport(transport);
+ this.transport = debugTransport;
+
this.client = new Client(
{
name: 'example-remote-client',
@@ -532,19 +501,23 @@ export class MCPConnectionManager {
capabilities: {},
}
);
-
- console.log('Connecting client to transport...');
- console.log('Transport type:', transport.constructor.name);
- console.log('Transport details:', transport);
- await this.client.connect(transport);
+ // Set up message callbacks to broadcast to UI after client is created
+ debugTransport.onsendmessage_ = async (message, options) => {
+ if (this.onMessage && this.client) {
+ this.onMessage(this.connection.id, this.client, message, 'sent', { options });
+ }
+ };
+
+ debugTransport.onreceivemessage_ = (message, extra) => {
+ if (this.onMessage && this.client) {
+ this.onMessage(this.connection.id, this.client, message, 'received', extra);
+ }
+ };
+
+ await this.client.connect(debugTransport);
this.connection.client = this.client;
- console.log('Client connected successfully');
} catch (error) {
- console.error('Client initialization failed:', error);
- console.error('Error type:', error?.constructor?.name);
- console.error('Error message:', error instanceof Error ? error.message : error);
- console.error('Full error object:', error);
throw error;
}
}
@@ -683,7 +656,6 @@ export class MCPConnectionManager {
await this.performHealthCheck();
}, 30000);
- console.log(`Started health check monitoring for ${this.connection.name}`);
}
private async performHealthCheck(): Promise {
@@ -695,7 +667,6 @@ export class MCPConnectionManager {
try {
// Try to list tools as a health check - this is a lightweight operation
await this.client.listTools();
- console.log(`Health check passed for ${this.connection.name}`);
} catch (error) {
console.warn(`Health check failed for ${this.connection.name}:`, error);
await this.handleHealthCheckFailure(error);
@@ -703,7 +674,6 @@ export class MCPConnectionManager {
}
private async handleHealthCheckFailure(error: any): Promise {
- console.log(`Connection health check failed for ${this.connection.name}, attempting reconnection...`);
// Stop health check during reconnection attempt
if (this.healthCheckInterval) {
@@ -719,7 +689,6 @@ export class MCPConnectionManager {
try {
// Attempt reconnection
await this.connect();
- console.log(`Health check reconnection successful for ${this.connection.name}`);
} catch (reconnectError) {
console.error(`Health check reconnection failed for ${this.connection.name}:`, reconnectError);
// The connect method will handle scheduling retry attempts
diff --git a/src/mcp/debugTransport.ts b/src/mcp/debugTransport.ts
new file mode 100644
index 0000000..98d3214
--- /dev/null
+++ b/src/mcp/debugTransport.ts
@@ -0,0 +1,63 @@
+import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
+import type { Transport, TransportSendOptions } from "@modelcontextprotocol/sdk/shared/transport.d.ts";
+import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
+
+
+/**
+ * Debug transport for MCP that logs messages to the console.
+ * This is useful for debugging purposes and does not implement any actual transport logic.
+ */
+export class DebugTransport implements Transport {
+
+ // Allow observability of the transport lifecycle
+ onclose_?: () => void;
+ onerror_?: (error: Error) => void;
+ onreceivemessage_?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => void;
+ onsendmessage_?: (message: JSONRPCMessage, options?: TransportSendOptions) => Promise;
+
+ private innerTransport: Transport;
+ constructor(innerTransport: Transport) {
+ this.innerTransport = innerTransport;
+ this.innerTransport.onclose = () => {
+ if (this.onclose_) {
+ this.onclose_();
+ }
+ if (this.onclose) {
+ this.onclose();
+ }
+ };
+ this.innerTransport.onerror = (error: Error) => {
+ if (this.onerror_) {
+ this.onerror_(error);
+ }
+ if (this.onerror) {
+ this.onerror(error);
+ }
+ }
+ this.innerTransport.onmessage = (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => {
+ if (this.onreceivemessage_) {
+ this.onreceivemessage_(message, extra);
+ }
+ if (this.onmessage) {
+ this.onmessage(message, extra);
+ }
+ }
+ }
+ async start(): Promise {
+ await this.innerTransport.start();
+ }
+ async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise {
+ if (this.onsendmessage_) {
+ await this.onsendmessage_(message, options);
+ }
+ await this.innerTransport.send(message, options);
+ }
+ async close(): Promise {
+ await this.innerTransport.close();
+ }
+
+ // These are taken over by the MCP client
+ onclose?: () => void;
+ onerror?: (error: Error) => void;
+ onmessage?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => void;
+}
\ No newline at end of file
diff --git a/src/types/mcp.ts b/src/types/mcp.ts
index 896b439..6d81089 100644
--- a/src/types/mcp.ts
+++ b/src/types/mcp.ts
@@ -2,6 +2,9 @@
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
import type { Tool } from './inference';
+import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
+import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
+import type { TransportSendOptions } from '@modelcontextprotocol/sdk/shared/transport.d.ts';
export interface MCPServerConfig {
name: string; // User-provided display name
@@ -29,6 +32,15 @@ export interface MCPMessage {
error?: any;
}
+// Message callback types
+export type MCPMessageCallback = (
+ connectionId: string,
+ client: Client,
+ message: JSONRPCMessage,
+ direction: 'sent' | 'received',
+ extra?: { authInfo?: AuthInfo; options?: TransportSendOptions }
+) => void;
+
export interface MCPResource {
uri: string;
name: string;
@@ -129,4 +141,8 @@ export interface MCPContextValue {
// OAuth handling
handleOAuthCallback: (connectionId: string, authorizationCode: string) => Promise;
+
+ // Message monitoring
+ addMessageCallback: (callback: MCPMessageCallback) => string; // Returns callback ID
+ removeMessageCallback: (callbackId: string) => void;
}
\ No newline at end of file
From 4771d8f1c857cdf43e409ddd5db99db74e169eec Mon Sep 17 00:00:00 2001
From: Jerome
Date: Tue, 17 Jun 2025 15:02:22 +0100
Subject: [PATCH 21/33] Fix duplicate key warnings and improve inference
message details
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Fix duplicate React keys by using unique IDs for request/response/error messages
- Show complete JSON data in Request/Response/Error details sections
- Add Message Metadata section showing all message properties
- Remove debug console.log statements
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/components/InferenceMessageMonitor.tsx | 186 +++++++++++++++++++++
src/components/InferenceTab.tsx | 173 +++++++++++--------
src/components/MCPMessageMonitor.tsx | 56 +------
src/contexts/InferenceContext.tsx | 123 +++++++++++++-
src/contexts/MCPContext.tsx | 48 +++++-
src/hooks/useAgentLoop.ts | 4 +-
src/types/inference.ts | 20 +++
src/types/mcp.ts | 13 ++
8 files changed, 496 insertions(+), 127 deletions(-)
create mode 100644 src/components/InferenceMessageMonitor.tsx
diff --git a/src/components/InferenceMessageMonitor.tsx b/src/components/InferenceMessageMonitor.tsx
new file mode 100644
index 0000000..2bec1be
--- /dev/null
+++ b/src/components/InferenceMessageMonitor.tsx
@@ -0,0 +1,186 @@
+// Component to monitor and display inference messages
+
+import { useState, useRef } from 'react';
+import { useInference } from '@/contexts/InferenceContext';
+import type { InferenceMessage } from '@/types/inference';
+
+export function InferenceMessageMonitor() {
+ const { messages, clearMessages } = useInference();
+ const [maxMessages, setMaxMessages] = useState(50);
+ const messagesEndRef = useRef(null);
+
+ // Slice messages to show only the latest maxMessages
+ const displayMessages = messages.slice(-maxMessages);
+
+ const getMessageTypeColor = (type: InferenceMessage['type']) => {
+ switch (type) {
+ case 'request':
+ return 'bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-800';
+ case 'response':
+ return 'bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-800';
+ case 'error':
+ return 'bg-red-50 dark:bg-red-900/30 border-red-200 dark:border-red-800';
+ case 'stream_chunk':
+ return 'bg-yellow-50 dark:bg-yellow-900/30 border-yellow-200 dark:border-yellow-800';
+ default:
+ return 'bg-gray-50 dark:bg-gray-900/30 border-gray-200 dark:border-gray-800';
+ }
+ };
+
+ const formatDuration = (duration?: number) => {
+ if (!duration) return '';
+ if (duration < 1000) return `${duration}ms`;
+ return `${(duration / 1000).toFixed(2)}s`;
+ };
+
+ const formatMessage = (message: InferenceMessage) => {
+ switch (message.type) {
+ case 'request':
+ return `Request to ${message.model || 'default model'}`;
+ case 'response':
+ return `Response from ${message.model || 'default model'}`;
+ case 'error':
+ return `Error from ${message.model || 'default model'}`;
+ case 'stream_chunk':
+ return `Stream chunk from ${message.model || 'default model'}`;
+ default:
+ return 'Unknown message type';
+ }
+ };
+
+ return (
+
+
+
+ Inference Monitor ({displayMessages.length})
+
+
+ Clear
+
+
+
+
+ {displayMessages.length === 0 ? (
+
+ No messages yet. Messages will appear here as inference requests are made.
+
+ ) : (
+
+ {displayMessages.map((message) => (
+
+
+
+
+ {message.type === 'request' ? '→' :
+ message.type === 'response' ? '←' :
+ message.type === 'error' ? '✗' : '⋯'}
+
+
+ {formatMessage(message)}
+
+ {message.duration && (
+
+ ({formatDuration(message.duration)})
+
+ )}
+
+
+ {message.timestamp.toLocaleTimeString()}
+
+
+
+ {/* Message metadata */}
+
+
+ Message Metadata
+
+
+ {JSON.stringify({
+ id: message.id,
+ type: message.type,
+ providerId: message.providerId,
+ providerName: message.providerName,
+ model: message.model,
+ timestamp: message.timestamp.toISOString(),
+ duration: message.duration
+ }, null, 2)}
+
+
+
+ {/* Request details */}
+ {message.request && (
+
+
+ Request Details
+
+
+ {JSON.stringify(message.request, null, 2)}
+
+
+ )}
+
+ {/* Response details */}
+ {message.response && (
+
+
+ Response Details
+
+
+ {JSON.stringify(message.response, null, 2)}
+
+
+ )}
+
+ {/* Error details */}
+ {message.error && (
+
+
+ Error Details
+
+
+ {message.error instanceof Error ? message.error.message : JSON.stringify(message.error, null, 2)}
+
+
+ )}
+
+ ))}
+
+
+ )}
+
+
+ {/* Controls */}
+
+
+
+ Max messages:
+
+ setMaxMessages(Number(e.target.value))}
+ className="px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ >
+ 25
+ 50
+ 100
+ 200
+
+
+
+ Live monitoring active
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/InferenceTab.tsx b/src/components/InferenceTab.tsx
index df8f9ae..c94bffd 100644
--- a/src/components/InferenceTab.tsx
+++ b/src/components/InferenceTab.tsx
@@ -3,10 +3,13 @@
import { useState } from 'react';
import { useInference } from '@/contexts/InferenceContext';
import { OpenRouterOAuthProvider } from '@/providers/openrouter/oauth-provider';
+import { InferenceMessageMonitor } from './InferenceMessageMonitor';
export function InferenceTab() {
const { provider, models, clearProvider, setProvider } = useInference();
const [isConnecting, setIsConnecting] = useState(false);
+ const [showConnectionStatus, setShowConnectionStatus] = useState(true);
+ const [showModels, setShowModels] = useState(false);
const handleConnect = async () => {
setIsConnecting(true);
@@ -28,88 +31,112 @@ export function InferenceTab() {
};
return (
-
-
- {/* Connection Status */}
-
-
- Inference Provider
-
-
- {provider ? (
-
-
-
-
- 🟢 Connected to OpenRouter
-
-
- {models.length} models available
-
-
-
- Disconnect
-
-
-
- ) : (
-
-
- ⚪ Not connected
-
-
- {isConnecting ? 'Connecting...' : 'Connect to OpenRouter'}
-
-
- )}
-
-
- {/* Available Models */}
- {provider && models.length > 0 && (
-
-
- Available Models ({models.length})
+
+ {/* Connection Status */}
+
+
setShowConnectionStatus(!showConnectionStatus)}
+ className="w-full p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
+ >
+
+
+ Connection Status
-
- {models.map((model) => (
-
-
- {model.id}
-
- {model.name && model.name !== model.id && (
-
- {model.name}
+
+ {showConnectionStatus ? '−' : '+'}
+
+
+
+ {showConnectionStatus && (
+
+
+ {provider ? (
+
+
+
+
+ 🟢 Connected to {provider.name}
+
+
+ {models.length} models available
+
- )}
-
- Context: {model.contextLength?.toLocaleString() || 'Unknown'}
+
+ Disconnect
+
+
+
+ ) : (
+
+
+ ⚪ Not connected
+
+
+ {isConnecting ? 'Connecting...' : 'Connect to OpenRouter'}
+
+
+
Connect to OpenRouter to access AI models for conversations.
+
You'll need an OpenRouter account and API key to authenticate.
- ))}
+ )}
)}
+
- {/* Help Text */}
-
-
- Connect to OpenRouter to access AI models for conversations.
-
-
- You'll need an OpenRouter account and API key to authenticate.
-
+ {/* Available Models */}
+ {provider && models.length > 0 && (
+
+
setShowModels(!showModels)}
+ className="w-full p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
+ >
+
+
+ Available Models ({models.length})
+
+
+ {showModels ? '−' : '+'}
+
+
+
+ {showModels && (
+
+
+ {models.map((model) => (
+
+
+ {model.id}
+
+ {model.name && model.name !== model.id && (
+
+ {model.name}
+
+ )}
+
+ Context: {model.contextLength?.toLocaleString() || 'Unknown'}
+
+
+ ))}
+
+
+ )}
-
+ )}
+
+ {/* Inference Message Monitor */}
+
);
}
\ No newline at end of file
diff --git a/src/components/MCPMessageMonitor.tsx b/src/components/MCPMessageMonitor.tsx
index 8aefdc3..b1ab550 100644
--- a/src/components/MCPMessageMonitor.tsx
+++ b/src/components/MCPMessageMonitor.tsx
@@ -1,53 +1,16 @@
// Component to monitor and display MCP messages
-import { useState, useEffect, useRef } from 'react';
+import { useState, useRef } from 'react';
import { useMCP } from '@/contexts/MCPContext';
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
-interface MCPMessageEntry {
- id: string;
- timestamp: Date;
- connectionId: string;
- connectionName: string;
- direction: 'sent' | 'received';
- message: JSONRPCMessage;
- extra?: any;
-}
-
export function MCPMessageMonitor() {
- const { addMessageCallback, removeMessageCallback, connections } = useMCP();
- const [messages, setMessages] = useState
([]);
+ const { messages, clearMessages } = useMCP();
const [maxMessages, setMaxMessages] = useState(50);
const messagesEndRef = useRef(null);
- // Remove auto-scroll - let user control scroll position
-
- // Register message callback
- useEffect(() => {
- const callbackId = addMessageCallback((connectionId, _client, message, direction, extra) => {
- const connection = connections.find(c => c.id === connectionId);
-
- setMessages(prev => {
- const newMessage: MCPMessageEntry = {
- id: `${Date.now()}-${Math.random()}`,
- timestamp: new Date(),
- connectionId,
- connectionName: connection?.name || 'Unknown',
- direction,
- message,
- extra,
- };
-
- // Keep only the latest maxMessages
- const newMessages = [...prev, newMessage];
- return newMessages.slice(-maxMessages);
- });
- });
-
- return () => {
- removeMessageCallback(callbackId);
- };
- }, [addMessageCallback, removeMessageCallback, connections, maxMessages]);
+ // Slice messages to show only the latest maxMessages
+ const displayMessages = messages.slice(-maxMessages);
const formatMessage = (msg: JSONRPCMessage) => {
if ('method' in msg) {
@@ -74,15 +37,12 @@ export function MCPMessageMonitor() {
return baseClass;
};
- const clearMessages = () => {
- setMessages([]);
- };
return (
- Message Monitor ({messages.length})
+ Message Monitor ({displayMessages.length})
- {messages.length === 0 ? (
+ {displayMessages.length === 0 ? (
No messages yet. Messages will appear here as they're sent/received.
) : (
- {messages.map((entry) => (
+ {displayMessages.map((entry) => (
- {entry.connectionName}
+ {entry.serverName}
diff --git a/src/contexts/InferenceContext.tsx b/src/contexts/InferenceContext.tsx
index 48a35a8..789fc33 100644
--- a/src/contexts/InferenceContext.tsx
+++ b/src/contexts/InferenceContext.tsx
@@ -6,7 +6,10 @@ import type {
InferenceRequest,
InferenceResponse,
Model,
+ InferenceMessage,
+ InferenceMessageCallback,
} from '@/types/inference';
+import { v4 as uuidv4 } from 'uuid';
import { OpenRouterApiProvider } from '@/providers/openrouter/api-provider';
import { OpenRouterOAuthProvider } from '@/providers/openrouter/oauth-provider';
@@ -30,6 +33,12 @@ interface InferenceContextValue {
models: Model[];
selectedModel: Model | undefined;
isAuthenticated: boolean;
+
+ // Message monitoring
+ messages: InferenceMessage[];
+ addMessageCallback: (callback: InferenceMessageCallback) => string; // Returns callback ID
+ removeMessageCallback: (callbackId: string) => void;
+ clearMessages: () => void;
}
const InferenceContext = createContext(null);
@@ -47,6 +56,22 @@ export function InferenceContextProvider({ children }: InferenceProviderProps) {
return localStorage.getItem('selected_model_id') || undefined;
});
const [_, setAuthStateVersion] = useState(0); // Force re-renders on auth changes
+
+ // Message callback management
+ const [messageCallbacks, setMessageCallbacks] = useState>(new Map());
+
+ // Store messages in context for persistence
+ const [messages, setMessages] = useState([]);
+ const maxMessages = 100; // Keep last 100 messages
+
+ // Function to add a message to the stored messages
+ const addMessage = useCallback((message: InferenceMessage) => {
+ setMessages(prev => {
+ const newMessages = [...prev, message];
+ // Keep only the latest maxMessages
+ return newMessages.slice(-maxMessages);
+ });
+ }, []);
const setProvider = useCallback((newProvider: InferenceProvider) => {
setProviderState(newProvider);
@@ -136,18 +161,91 @@ export function InferenceContextProvider({ children }: InferenceProviderProps) {
setIsLoading(true);
setError(null);
+
+ const requestId = uuidv4();
+ const startTime = Date.now();
+
+ // Broadcast request message
+ const requestMessage: InferenceMessage = {
+ id: `${requestId}-request`,
+ timestamp: new Date(),
+ type: 'request',
+ providerId: provider.id,
+ providerName: provider.name,
+ model: request.model,
+ request,
+ };
+
+ // Store message in context
+ addMessage(requestMessage);
+
+ messageCallbacks.forEach(callback => {
+ try {
+ callback(requestMessage);
+ } catch (error) {
+ console.error('Error in inference message callback:', error);
+ }
+ });
try {
const response = await provider.generateResponse(request);
+
+ // Broadcast response message
+ const responseMessage: InferenceMessage = {
+ id: `${requestId}-response`,
+ timestamp: new Date(),
+ type: 'response',
+ providerId: provider.id,
+ providerName: provider.name,
+ model: request.model,
+ response,
+ duration: Date.now() - startTime,
+ };
+
+ // Store message in context
+ addMessage(responseMessage);
+
+ messageCallbacks.forEach(callback => {
+ try {
+ callback(responseMessage);
+ } catch (error) {
+ console.error('Error in inference message callback:', error);
+ }
+ });
+
return response;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Inference request failed';
setError(errorMessage);
+
+ // Broadcast error message
+ const errorMessageObj: InferenceMessage = {
+ id: `${requestId}-error`,
+ timestamp: new Date(),
+ type: 'error',
+ providerId: provider.id,
+ providerName: provider.name,
+ model: request.model,
+ error: err,
+ duration: Date.now() - startTime,
+ };
+
+ // Store message in context
+ addMessage(errorMessageObj);
+
+ messageCallbacks.forEach(callback => {
+ try {
+ callback(errorMessageObj);
+ } catch (error) {
+ console.error('Error in inference message callback:', error);
+ }
+ });
+
throw err;
} finally {
setIsLoading(false);
}
- }, [provider]);
+ }, [provider, messageCallbacks, addMessage]);
const selectModel = useCallback((modelId: string) => {
if (!provider) {
@@ -209,6 +307,25 @@ export function InferenceContextProvider({ children }: InferenceProviderProps) {
}
}, [provider]);
+ // Message callback management
+ const addMessageCallback = useCallback((callback: InferenceMessageCallback): string => {
+ const callbackId = uuidv4();
+ setMessageCallbacks(prev => new Map(prev).set(callbackId, callback));
+ return callbackId;
+ }, []);
+
+ const removeMessageCallback = useCallback((callbackId: string) => {
+ setMessageCallbacks(prev => {
+ const newMap = new Map(prev);
+ newMap.delete(callbackId);
+ return newMap;
+ });
+ }, []);
+
+ const clearMessages = useCallback(() => {
+ setMessages([]);
+ }, []);
+
const contextValue: InferenceContextValue = {
provider,
isLoading,
@@ -222,6 +339,10 @@ export function InferenceContextProvider({ children }: InferenceProviderProps) {
models: provider?.models || [],
selectedModel: selectedModelId ? provider?.models.find(m => m.id === selectedModelId) : undefined,
isAuthenticated: provider?.isAuthenticated || false,
+ messages,
+ addMessageCallback,
+ removeMessageCallback,
+ clearMessages,
};
diff --git a/src/contexts/MCPContext.tsx b/src/contexts/MCPContext.tsx
index 4c3e42b..ede9045 100644
--- a/src/contexts/MCPContext.tsx
+++ b/src/contexts/MCPContext.tsx
@@ -9,6 +9,7 @@ import type {
MCPResource,
MCPContextValue,
MCPMessageCallback,
+ MCPMonitorMessage,
} from '@/types/mcp';
import type { Tool } from '@/types/inference';
import { MCPConnectionManager } from '@/mcp/connection';
@@ -25,15 +26,33 @@ export function MCPProvider({ children }: MCPProviderProps) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const hasLoadedPersisted = useRef(false);
+ const connectionsRef = useRef([]);
// Message callback management
const [messageCallbacks, setMessageCallbacks] = useState>(new Map());
const messageCallbacksRef = useRef>(new Map());
- // Keep ref in sync with state
+ // Store messages for persistence
+ const [messages, setMessages] = useState([]);
+ const maxMessages = 100; // Keep last 100 messages
+
+ // Keep refs in sync with state
useEffect(() => {
messageCallbacksRef.current = messageCallbacks;
}, [messageCallbacks]);
+
+ useEffect(() => {
+ connectionsRef.current = connections;
+ }, [connections]);
+
+ // Function to add a message to the stored messages
+ const addMessage = useCallback((message: MCPMonitorMessage) => {
+ setMessages(prev => {
+ const newMessages = [...prev, message];
+ // Keep only the latest maxMessages
+ return newMessages.slice(-maxMessages);
+ });
+ }, []);
// Function to broadcast messages to all callbacks (stable reference)
const broadcastMessage = useCallback((
@@ -43,7 +62,24 @@ export function MCPProvider({ children }: MCPProviderProps) {
direction: 'sent' | 'received',
extra?: any
) => {
- console.log(`MCP Message [${direction}] from ${connectionId}:`, message); // Debug log
+ // console.log(`MCP Message [${direction}] from ${connectionId}:`, message); // Debug log
+
+ // Find connection name using ref to get latest state
+ const connection = connectionsRef.current.find(conn => conn.id === connectionId);
+ const serverName = connection?.name || 'Unknown Server';
+
+ // Store message
+ const storedMessage: MCPMonitorMessage = {
+ id: uuidv4(),
+ timestamp: new Date(),
+ connectionId,
+ serverName,
+ message,
+ direction,
+ extra,
+ };
+ addMessage(storedMessage);
+
messageCallbacksRef.current.forEach(callback => {
try {
callback(connectionId, client, message, direction, extra);
@@ -51,7 +87,7 @@ export function MCPProvider({ children }: MCPProviderProps) {
console.error('Error in MCP message callback:', error);
}
});
- }, []); // No dependencies - uses ref
+ }, [addMessage]); // Only depends on addMessage, uses ref for connections
// Load persisted connections from localStorage on mount
useEffect(() => {
@@ -384,6 +420,10 @@ export function MCPProvider({ children }: MCPProviderProps) {
return newMap;
});
}, []);
+
+ const clearMessages = useCallback(() => {
+ setMessages([]);
+ }, []);
const contextValue: MCPContextValue = {
connections,
@@ -402,8 +442,10 @@ export function MCPProvider({ children }: MCPProviderProps) {
getConnectionById,
updateServerConfig,
handleOAuthCallback,
+ messages,
addMessageCallback,
removeMessageCallback,
+ clearMessages,
};
return (
diff --git a/src/hooks/useAgentLoop.ts b/src/hooks/useAgentLoop.ts
index 61b4ef6..af927bd 100644
--- a/src/hooks/useAgentLoop.ts
+++ b/src/hooks/useAgentLoop.ts
@@ -126,7 +126,7 @@ const DEFAULT_CONFIG: AgentLoopConfig = {
export function useAgentLoop(config: Partial = {}): UseAgentLoopReturn {
const finalConfig = { ...DEFAULT_CONFIG, ...config };
- const { provider: currentProvider, isAuthenticated } = useInference();
+ const { provider: currentProvider, isAuthenticated, generateResponse } = useInference();
const { getAllTools, callTool: callMCPTool, connections } = useMCP();
// Track running loops
@@ -309,7 +309,7 @@ export function useAgentLoop(config: Partial = {}): UseAgentLoo
temperature: finalConfig.temperature,
};
- const response = await currentProvider.generateResponse(request);
+ const response = await generateResponse(request);
if (abortController.signal.aborted) {
break;
diff --git a/src/types/inference.ts b/src/types/inference.ts
index 6f04183..37eba51 100644
--- a/src/types/inference.ts
+++ b/src/types/inference.ts
@@ -1,5 +1,22 @@
// Core inference types and interfaces
+// Inference message monitoring types
+export interface InferenceMessage {
+ id: string;
+ timestamp: Date;
+ type: 'request' | 'response' | 'error' | 'stream_chunk';
+ providerId: string;
+ providerName: string;
+ model?: string;
+ request?: InferenceRequest;
+ response?: InferenceResponse;
+ streamChunk?: any;
+ error?: any;
+ duration?: number; // Response time in ms
+}
+
+export type InferenceMessageCallback = (message: InferenceMessage) => void;
+
export interface Model {
id: string;
name: string;
@@ -49,6 +66,8 @@ export interface Tool {
export interface InferenceRequest {
messages: ChatMessage[];
+ model?: string;
+ systemPrompt?: string;
tools?: Tool[]; // toolChoice defaults to 'auto' when tools provided
maxTokens?: number;
temperature?: number;
@@ -59,6 +78,7 @@ export interface InferenceResponse {
message: ChatMessage;
usage: TokenUsage;
stopReason: 'stop' | 'max_tokens' | 'tool_calls' | 'error';
+ finishReason?: string;
error?: string;
}
diff --git a/src/types/mcp.ts b/src/types/mcp.ts
index 6d81089..a1dd3c6 100644
--- a/src/types/mcp.ts
+++ b/src/types/mcp.ts
@@ -41,6 +41,17 @@ export type MCPMessageCallback = (
extra?: { authInfo?: AuthInfo; options?: TransportSendOptions }
) => void;
+// MCP message for storage/monitoring
+export interface MCPMonitorMessage {
+ id: string;
+ timestamp: Date;
+ connectionId: string;
+ serverName: string;
+ message: JSONRPCMessage;
+ direction: 'sent' | 'received';
+ extra?: { authInfo?: AuthInfo; options?: TransportSendOptions };
+}
+
export interface MCPResource {
uri: string;
name: string;
@@ -143,6 +154,8 @@ export interface MCPContextValue {
handleOAuthCallback: (connectionId: string, authorizationCode: string) => Promise;
// Message monitoring
+ messages: MCPMonitorMessage[];
addMessageCallback: (callback: MCPMessageCallback) => string; // Returns callback ID
removeMessageCallback: (callbackId: string) => void;
+ clearMessages: () => void;
}
\ No newline at end of file
From 7872b5fdb71eef3c176602c09039dd1a4cf8ffae Mon Sep 17 00:00:00 2001
From: Jerome
Date: Wed, 18 Jun 2025 13:53:42 +0100
Subject: [PATCH 22/33] Using ping for the keep alive, doing it on a 10min
cycle
---
src/mcp/connection.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/mcp/connection.ts b/src/mcp/connection.ts
index 1c25379..7acc523 100644
--- a/src/mcp/connection.ts
+++ b/src/mcp/connection.ts
@@ -654,7 +654,7 @@ export class MCPConnectionManager {
// Start health check every 30 seconds
this.healthCheckInterval = setInterval(async () => {
await this.performHealthCheck();
- }, 30000);
+ }, 10 * 60 * 1000); // 10 minutes
}
@@ -666,7 +666,7 @@ export class MCPConnectionManager {
try {
// Try to list tools as a health check - this is a lightweight operation
- await this.client.listTools();
+ await this.client.ping();
} catch (error) {
console.warn(`Health check failed for ${this.connection.name}:`, error);
await this.handleHealthCheckFailure(error);
From 728fea3665b5ebafea99e7254942084d45324aab Mon Sep 17 00:00:00 2001
From: Jerome
Date: Wed, 18 Jun 2025 18:33:55 +0100
Subject: [PATCH 23/33] Simplify MCP connection handling and remove health
checks
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Remove auth type selection from UI - servers will auto-detect OAuth when needed
- Remove auto-reconnect logic and health checks to reduce complexity
- Always initialize OAuth provider for all connections
- Simplify transport selection - always try streamable-http first, then SSE
- Remove unused config options (transport preference, auto-reconnect)
- Add TODO comment about CORS issues for failed fetch errors
- Make authType and transport optional in connection types
- Clean up connection state notifications for better UI updates
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/components/MCPTab.tsx | 19 ----
src/mcp/connection.ts | 233 +++++++++++---------------------------
src/types/mcp.ts | 6 +-
3 files changed, 65 insertions(+), 193 deletions(-)
diff --git a/src/components/MCPTab.tsx b/src/components/MCPTab.tsx
index c77f497..c8f136d 100644
--- a/src/components/MCPTab.tsx
+++ b/src/components/MCPTab.tsx
@@ -9,7 +9,6 @@ export function MCPTab() {
const [showAddForm, setShowAddForm] = useState(false);
const [newServerName, setNewServerName] = useState('');
const [newServerUrl, setNewServerUrl] = useState('');
- const [authType, setAuthType] = useState<'none' | 'oauth'>('none');
// Collapsible section states
const [showSummary, setShowSummary] = useState(true);
@@ -21,7 +20,6 @@ export function MCPTab() {
await addMcpServer({
name: 'Example Server',
url: 'https://example-server.modelcontextprotocol.io/sse',
- authType: 'oauth',
});
} catch (error) {
console.error('Failed to add example server:', error);
@@ -36,13 +34,11 @@ export function MCPTab() {
await addMcpServer({
name: newServerName,
url: newServerUrl,
- authType,
});
// Reset form
setNewServerName('');
setNewServerUrl('');
- setAuthType('none');
setShowAddForm(false);
} catch (error) {
console.error('Failed to add custom server:', error);
@@ -175,21 +171,6 @@ export function MCPTab() {
required
/>
-
-
-
- Authentication
-
- setAuthType(e.target.value as 'none' | 'oauth')}
- className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
- >
- None
- OAuth
-
-
-
void;
-
+
constructor(connectionId: string, serverName: string, serverUrl: string, onOAuthComplete?: () => void) {
this.connectionId = connectionId;
this.serverName = serverName;
this.serverUrl = serverUrl;
this.onOAuthComplete = onOAuthComplete;
}
-
+
get redirectUrl(): string {
return `${window.location.origin}/oauth/mcp/callback`;
}
-
+
get clientMetadata(): OAuthClientMetadata {
return {
redirect_uris: [this.redirectUrl],
@@ -81,13 +81,13 @@ class MCPOAuthProvider implements OAuthClientProvider {
token_endpoint_auth_method: 'none', // Public client
};
}
-
+
state(): string {
// Encode connection ID in the state parameter for callback identification
const randomPart = this.generateRandomString(8);
return `${this.connectionId}.${randomPart}`;
}
-
+
// Generate a consistent key for the server based on URL
getServerKey(): string {
try {
@@ -106,23 +106,23 @@ class MCPOAuthProvider implements OAuthClientProvider {
const stored = localStorage.getItem(`mcp_oauth_client_${serverKey}`);
return stored ? JSON.parse(stored) : undefined;
}
-
+
async saveClientInformation(clientInformation: OAuthClientInformation): Promise {
const serverKey = this.getServerKey();
localStorage.setItem(`mcp_oauth_client_${serverKey}`, JSON.stringify(clientInformation));
}
-
+
tokens(): OAuthTokens | undefined {
const stored = localStorage.getItem(`mcp_oauth_tokens_${this.connectionId}`);
return stored ? JSON.parse(stored) : undefined;
}
-
+
async saveTokens(tokens: OAuthTokens): Promise {
localStorage.setItem(`mcp_oauth_tokens_${this.connectionId}`, JSON.stringify(tokens));
}
-
+
async redirectToAuthorization(authorizationUrl: URL): Promise {
-
+
// Open popup for OAuth flow
const popup = window.open(
authorizationUrl.toString(),
@@ -138,7 +138,7 @@ class MCPOAuthProvider implements OAuthClientProvider {
const handleMessage = (event: MessageEvent) => {
if (event.origin !== window.location.origin) return;
if (event.data.type !== 'mcp_oauth_callback') return;
-
+
// Extract connection ID from state parameter
const state = event.data.state;
if (!state || !state.startsWith(this.connectionId + '.')) return;
@@ -157,7 +157,7 @@ class MCPOAuthProvider implements OAuthClientProvider {
window.addEventListener('message', handleMessage);
}
-
+
async saveCodeVerifier(codeVerifier: string): Promise {
const oauthState: MCPOAuthState = {
codeVerifier,
@@ -166,22 +166,22 @@ class MCPOAuthProvider implements OAuthClientProvider {
};
localStorage.setItem(`mcp_oauth_state_${this.connectionId}`, JSON.stringify(oauthState));
}
-
+
async codeVerifier(): Promise {
const stored = localStorage.getItem(`mcp_oauth_state_${this.connectionId}`);
if (!stored) {
throw new Error('No OAuth state found');
}
-
+
const oauthState: MCPOAuthState = JSON.parse(stored);
if (Date.now() > oauthState.expiresAt) {
localStorage.removeItem(`mcp_oauth_state_${this.connectionId}`);
throw new Error('OAuth state expired');
}
-
+
return oauthState.codeVerifier;
}
-
+
private generateRandomString(length: number): string {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
@@ -199,7 +199,7 @@ class MCPOAuthProvider implements OAuthClientProvider {
serverUrl: this.serverUrl,
authorizationCode,
});
-
+
if (result === 'AUTHORIZED') {
this.notifyOAuthComplete();
} else {
@@ -230,7 +230,7 @@ class MCPOAuthProvider implements OAuthClientProvider {
export class MCPConnectionManager {
private connection: MCPConnection;
private client?: Client;
- private transport?: Transport;
+ private transport?: Transport | StreamableHTTPClientTransport | SSEClientTransport;
private reconnectTimeout?: NodeJS.Timeout;
private healthCheckInterval?: NodeJS.Timeout;
private oauthProvider?: MCPOAuthProvider;
@@ -243,22 +243,16 @@ export class MCPConnectionManager {
name: config.name,
url: config.url,
status: 'disconnected',
- transport: 'streamable-http', // Will be determined during connection
- authType: config.authType || 'none',
tools: [],
resources: [],
prompts: [],
connectionAttempts: 0,
config,
};
-
- // Initialize OAuth provider if auth is required
- if (this.connection.authType === 'oauth') {
- this.oauthProvider = new MCPOAuthProvider(id, config.name, config.url, () => {
- // Callback when OAuth completes successfully
- this.handleOAuthSuccess();
- });
- }
+ this.oauthProvider = new MCPOAuthProvider(id, config.name, config.url, () => {
+ // Callback when OAuth completes successfully
+ this.handleOAuthSuccess();
+ });
}
getConnection(): MCPConnection {
@@ -272,7 +266,6 @@ export class MCPConnectionManager {
// Set callback for message monitoring
setMessageCallback(callback: (connectionId: string, client: any, message: any, direction: 'sent' | 'received', extra?: any) => void): void {
- console.log(`Setting message callback for connection ${this.connection.id}`); // Debug log
this.onMessage = callback;
}
@@ -286,61 +279,37 @@ export class MCPConnectionManager {
async connect(): Promise {
this.connection.status = 'connecting';
this.connection.error = undefined;
-
+ this.notifyConnectionUpdate();
+
try {
-
+
// Clear any existing connections
await this.disconnect();
-
- // Determine transport strategy
- const transportPreference = this.connection.config.transport || 'auto';
-
- if (transportPreference === 'auto' || transportPreference === 'streamable-http') {
- try {
- await this.tryStreamableHttp();
- this.connection.transport = 'streamable-http';
- } catch (error) {
- if (transportPreference === 'streamable-http') {
- throw error; // Don't fallback if explicitly requested
- }
- // Try SSE fallback
- await this.trySSE();
- this.connection.transport = 'sse';
- }
- } else if (transportPreference === 'sse') {
+
+ try {
+ await this.tryStreamableHttp();
+ this.connection.transport = 'streamable-http';
+ } catch (error) {
+ // TODO: jerome - if this is a TypeError: failed to fetch, then there is likely a CORS (or
+ // Access-Control-Expose-Headers) issue with the server.
await this.trySSE();
this.connection.transport = 'sse';
}
// Initialize client capabilities
await this.initializeCapabilities();
-
+
this.connection.status = 'connected';
this.connection.lastConnected = new Date();
this.connection.connectionAttempts = 0;
-
- // Start health check monitoring
- this.startHealthCheck();
-
+ this.notifyConnectionUpdate();
+
} catch (error) {
this.connection.status = 'failed';
this.connection.error = error instanceof Error ? error.message : 'Unknown connection error';
this.connection.connectionAttempts++;
-
- // Schedule auto-reconnect if enabled
- if (this.connection.config.autoReconnect !== false &&
- this.connection.connectionAttempts < (this.connection.config.maxReconnectAttempts || 5)) {
- const delay = this.getBackoffDelay();
- this.reconnectTimeout = setTimeout(async () => {
- try {
- await this.reconnect();
- } catch (error) {
- console.error('Auto-reconnect failed:', error);
- // Don't throw here to prevent uncaught promise rejection
- }
- }, delay);
- }
-
+ this.notifyConnectionUpdate();
+
throw this.createMCPError('connection', this.connection.error, error);
}
}
@@ -350,12 +319,12 @@ export class MCPConnectionManager {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = undefined;
}
-
+
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
this.healthCheckInterval = undefined;
}
-
+
if (this.client) {
try {
await this.client.close();
@@ -364,7 +333,7 @@ export class MCPConnectionManager {
}
this.client = undefined;
}
-
+
if (this.transport) {
try {
await this.transport.close();
@@ -373,7 +342,7 @@ export class MCPConnectionManager {
}
this.transport = undefined;
}
-
+
this.connection.status = 'disconnected';
this.connection.client = undefined;
}
@@ -384,12 +353,12 @@ export class MCPConnectionManager {
throw new Error('OAuth provider not initialized');
}
-
+
try {
// If we have an active transport, use its finishAuth method
if (this.transport && typeof (this.transport as any).finishAuth === 'function') {
await (this.transport as any).finishAuth(authorizationCode);
-
+
// Now attempt to connect - the transport should be authenticated
await this.connect();
} else {
@@ -398,7 +367,7 @@ export class MCPConnectionManager {
this.oauthProvider.pendingAuthorizationCode = authorizationCode;
await this.connect();
}
-
+
} catch (error) {
console.error('OAuth callback processing failed:', error);
throw new Error(`OAuth callback failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -411,7 +380,7 @@ export class MCPConnectionManager {
// Clear connection-specific data
localStorage.removeItem(`mcp_oauth_tokens_${this.connection.id}`);
localStorage.removeItem(`mcp_oauth_state_${this.connection.id}`);
-
+
// Note: We intentionally don't clear client information here since it's
// shared across connections to the same server. Use clearSharedClientData()
// if you need to clear the client registration for this server.
@@ -432,57 +401,36 @@ export class MCPConnectionManager {
// Handle successful OAuth completion
private async handleOAuthSuccess(): Promise {
-
+
try {
- // Reset connection state and retry
- this.connection.status = 'connecting';
- this.connection.error = undefined;
- this.notifyConnectionUpdate();
-
// Attempt to connect now that we have valid tokens
await this.connect();
-
- this.notifyConnectionUpdate();
} catch (error) {
console.error('Post-OAuth connection failed:', error);
- this.connection.status = 'failed';
- this.connection.error = error instanceof Error ? error.message : 'Post-OAuth connection failed';
- this.notifyConnectionUpdate();
}
}
private async tryStreamableHttp(): Promise {
try {
-
- // Create transport options with OAuth provider if configured
- const transportOptions: any = {};
-
- if (this.connection.authType === 'oauth' && this.oauthProvider) {
- transportOptions.authProvider = this.oauthProvider;
- }
-
- const transport = new StreamableHTTPClientTransport(new URL(this.connection.url), transportOptions);
+ const transport = new StreamableHTTPClientTransport(new URL(this.connection.url), {
+ authProvider: this.oauthProvider
+ });
await this.initializeClient(transport);
} catch (error) {
- console.error('StreamableHTTP connection failed:', error);
+ console.log('StreamableHTTP connection failed:', error);
throw error;
}
}
private async trySSE(): Promise {
try {
-
- // Create transport options with OAuth provider if configured
- const transportOptions: any = {};
-
- if (this.connection.authType === 'oauth' && this.oauthProvider) {
- transportOptions.authProvider = this.oauthProvider;
- }
-
- const transport = new SSEClientTransport(new URL(this.connection.url), transportOptions);
+ const transport = new SSEClientTransport(new URL(this.connection.url), {
+ authProvider: this.oauthProvider
+ });
await this.initializeClient(transport);
} catch (error) {
+ console.log('SSE connection failed:', error);
throw error;
}
}
@@ -491,7 +439,7 @@ export class MCPConnectionManager {
try {
const debugTransport = new DebugTransport(transport);
this.transport = debugTransport;
-
+
this.client = new Client(
{
name: 'example-remote-client',
@@ -501,20 +449,20 @@ export class MCPConnectionManager {
capabilities: {},
}
);
-
+
// Set up message callbacks to broadcast to UI after client is created
debugTransport.onsendmessage_ = async (message, options) => {
if (this.onMessage && this.client) {
this.onMessage(this.connection.id, this.client, message, 'sent', { options });
}
};
-
+
debugTransport.onreceivemessage_ = (message, extra) => {
if (this.onMessage && this.client) {
this.onMessage(this.connection.id, this.client, message, 'received', extra);
}
};
-
+
await this.client.connect(debugTransport);
this.connection.client = this.client;
} catch (error) {
@@ -530,7 +478,7 @@ export class MCPConnectionManager {
try {
// Discover tools
this.connection.tools = await this.discoverTools();
-
+
// Discover resources (if supported)
try {
this.connection.resources = await this.discoverResources();
@@ -538,7 +486,7 @@ export class MCPConnectionManager {
// Resources not supported by this server
this.connection.resources = [];
}
-
+
// Discover prompts (if supported)
try {
this.connection.prompts = await this.discoverPrompts();
@@ -546,7 +494,7 @@ export class MCPConnectionManager {
// Prompts not supported by this server
this.connection.prompts = [];
}
-
+
} catch (error) {
throw this.createMCPError('protocol', 'Failed to initialize server capabilities', error);
}
@@ -620,7 +568,7 @@ export class MCPConnectionManager {
// Remove the server prefix from the tool name (using double underscore separator)
const normalizedServerName = normalizeServerName(this.connection.name);
- const unprefixedName = toolName.startsWith(`${normalizedServerName}__`)
+ const unprefixedName = toolName.startsWith(`${normalizedServerName}__`)
? toolName.slice(normalizedServerName.length + 2)
: toolName;
@@ -640,61 +588,6 @@ export class MCPConnectionManager {
return this.connection.status;
}
- private getBackoffDelay(): number {
- // Exponential backoff: 1s, 2s, 4s, 8s, 16s
- return Math.min(1000 * Math.pow(2, this.connection.connectionAttempts - 1), 16000);
- }
-
- private startHealthCheck(): void {
- // Clear any existing health check
- if (this.healthCheckInterval) {
- clearInterval(this.healthCheckInterval);
- }
-
- // Start health check every 30 seconds
- this.healthCheckInterval = setInterval(async () => {
- await this.performHealthCheck();
- }, 10 * 60 * 1000); // 10 minutes
-
- }
-
- private async performHealthCheck(): Promise {
- // Only check if we're supposed to be connected
- if (this.connection.status !== 'connected' || !this.client) {
- return;
- }
-
- try {
- // Try to list tools as a health check - this is a lightweight operation
- await this.client.ping();
- } catch (error) {
- console.warn(`Health check failed for ${this.connection.name}:`, error);
- await this.handleHealthCheckFailure(error);
- }
- }
-
- private async handleHealthCheckFailure(error: any): Promise {
-
- // Stop health check during reconnection attempt
- if (this.healthCheckInterval) {
- clearInterval(this.healthCheckInterval);
- this.healthCheckInterval = undefined;
- }
-
- // Mark as disconnected and attempt reconnection
- this.connection.status = 'connecting';
- this.connection.error = error instanceof Error ? error.message : 'Health check failed';
- this.notifyConnectionUpdate();
-
- try {
- // Attempt reconnection
- await this.connect();
- } catch (reconnectError) {
- console.error(`Health check reconnection failed for ${this.connection.name}:`, reconnectError);
- // The connect method will handle scheduling retry attempts
- }
- }
-
private createMCPError(type: MCPError['type'], message: string, details?: any): MCPError {
return {
type,
diff --git a/src/types/mcp.ts b/src/types/mcp.ts
index a1dd3c6..1eab0d3 100644
--- a/src/types/mcp.ts
+++ b/src/types/mcp.ts
@@ -9,7 +9,6 @@ import type { TransportSendOptions } from '@modelcontextprotocol/sdk/shared/tran
export interface MCPServerConfig {
name: string; // User-provided display name
url: string; // Server endpoint URL
- transport?: 'sse' | 'streamable-http' | 'auto'; // Default: auto-detect
authType?: 'none' | 'oauth'; // Default: none
oauthConfig?: {
clientId?: string;
@@ -18,7 +17,6 @@ export interface MCPServerConfig {
scope?: string; // OAuth scope
redirectUri?: string; // Override default redirect URI
};
- autoReconnect?: boolean; // Default: true
maxReconnectAttempts?: number; // Default: 5
}
@@ -75,8 +73,8 @@ export interface MCPConnection {
url: string; // Server URL
status: 'connecting' | 'connected' | 'failed' | 'disconnected';
client?: Client; // MCP SDK client instance
- transport: 'sse' | 'streamable-http';
- authType: 'none' | 'oauth';
+ transport?: 'sse' | 'streamable-http';
+ authType?: 'none' | 'oauth';
// Available capabilities
tools: Tool[]; // Tools with name-prefixed identifiers
From 8ae2da590f4dae4e466d78d49c92544296b7e753 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Wed, 18 Jun 2025 20:03:20 +0100
Subject: [PATCH 24/33] Enable static site deployment with OAuth callback
support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Update OAuth callbacks to use query parameters instead of path routing
- Change redirect URIs to use current page URL (works with any base path)
- Add callback type prefix to state parameter (inference: or mcp:)
- Update App.tsx to route OAuth callbacks based on query params
- Add base path configuration to vite.config.ts for GitHub Pages
- Now works with static hosting (GitHub Pages, python http.server, etc)
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/App.tsx | 25 ++++++++++++++--------
src/mcp/connection.ts | 6 +++---
src/providers/openrouter/oauth-provider.ts | 12 ++++++++---
vite.config.ts | 3 +++
4 files changed, 31 insertions(+), 15 deletions(-)
diff --git a/src/App.tsx b/src/App.tsx
index 48bc69e..1ce9a7d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -4,17 +4,24 @@ import { ConversationApp } from '@/components/ConversationApp'
import { OAuthCallback } from '@/components/OAuthCallback'
function App() {
- // Simple routing based on pathname
- const pathname = window.location.pathname;
- const isInferenceOAuthCallback = pathname === '/oauth/inference/callback';
- const isMcpOAuthCallback = pathname === '/oauth/mcp/callback';
-
- if (isInferenceOAuthCallback) {
- return ;
+ // Check if this is an OAuth callback based on query parameters
+ const urlParams = new URLSearchParams(window.location.search);
+ const code = urlParams.get('code');
+ const state = urlParams.get('state');
+
+ // Determine OAuth callback type from state parameter
+ let oauthType: 'inference' | 'mcp' | null = null;
+ if (code && state) {
+ // The state parameter includes the callback type
+ if (state.includes('inference:')) {
+ oauthType = 'inference';
+ } else if (state.includes('mcp:')) {
+ oauthType = 'mcp';
+ }
}
- if (isMcpOAuthCallback) {
- return ;
+ if (oauthType) {
+ return ;
}
return (
diff --git a/src/mcp/connection.ts b/src/mcp/connection.ts
index 22ec803..e00cadd 100644
--- a/src/mcp/connection.ts
+++ b/src/mcp/connection.ts
@@ -69,7 +69,7 @@ class MCPOAuthProvider implements OAuthClientProvider {
}
get redirectUrl(): string {
- return `${window.location.origin}/oauth/mcp/callback`;
+ return `${window.location.origin}${window.location.pathname}`;
}
get clientMetadata(): OAuthClientMetadata {
@@ -85,7 +85,7 @@ class MCPOAuthProvider implements OAuthClientProvider {
state(): string {
// Encode connection ID in the state parameter for callback identification
const randomPart = this.generateRandomString(8);
- return `${this.connectionId}.${randomPart}`;
+ return `mcp:${this.connectionId}.${randomPart}`;
}
// Generate a consistent key for the server based on URL
@@ -141,7 +141,7 @@ class MCPOAuthProvider implements OAuthClientProvider {
// Extract connection ID from state parameter
const state = event.data.state;
- if (!state || !state.startsWith(this.connectionId + '.')) return;
+ if (!state || !state.startsWith(`mcp:${this.connectionId}.`)) return;
window.removeEventListener('message', handleMessage);
popup.close();
diff --git a/src/providers/openrouter/oauth-provider.ts b/src/providers/openrouter/oauth-provider.ts
index abbfb3f..15f8623 100644
--- a/src/providers/openrouter/oauth-provider.ts
+++ b/src/providers/openrouter/oauth-provider.ts
@@ -33,7 +33,7 @@ export class OpenRouterOAuthProvider extends InferenceProvider {
constructor(config?: OpenRouterOAuthConfig) {
super();
this.oauthConfig = {
- redirectUri: `${window.location.origin}/oauth/inference/callback`,
+ redirectUri: `${window.location.origin}${window.location.pathname}`,
...config,
};
this.client = new OpenRouterClient(config || {});
@@ -253,7 +253,11 @@ export class OpenRouterOAuthProvider extends InferenceProvider {
}
const storedState: OAuthState = JSON.parse(storedStateJson);
- if (storedState.state !== state || Date.now() > storedState.expiresAt) {
+ // Extract the actual state without the prefix for comparison
+ const stateWithoutPrefix = state.replace(/^inference:/, '');
+ const storedStateWithoutPrefix = storedState.state.replace(/^inference:/, '');
+
+ if (storedStateWithoutPrefix !== stateWithoutPrefix || Date.now() > storedState.expiresAt) {
localStorage.removeItem('openrouter_oauth_state');
throw new Error('Invalid or expired OAuth state');
}
@@ -319,7 +323,9 @@ export class OpenRouterOAuthProvider extends InferenceProvider {
private generateState(): string {
const array = new Uint8Array(16);
crypto.getRandomValues(array);
- return btoa(String.fromCharCode.apply(null, Array.from(array)));
+ const randomPart = btoa(String.fromCharCode.apply(null, Array.from(array)));
+ // Include callback type in state for routing
+ return `inference:${randomPart}`;
}
private storeTokens(): void {
diff --git a/vite.config.ts b/vite.config.ts
index 2d42021..f4734f6 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -5,6 +5,9 @@ import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
+ // Set base to '/' for local testing, or to your repo name for GitHub Pages
+ // e.g., base: '/example-remote-client/' for https://username.github.io/example-remote-client/
+ base: '/',
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
From c9feacd76323d35686e25ccaca0f0c54d7a6ab30 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 19 Jun 2025 13:40:47 +0100
Subject: [PATCH 25/33] Reduce OAuth state timeout from 10 to 5 minutes for
improved security
- Update MCP OAuth state expiration in connection.ts
- Update OpenRouter OAuth state expiration in oauth-provider.ts
- Addresses security review feedback to minimize window for potential attacks
---
docs/security_design_doc.md | 159 +++++++++++++++++++++
src/hooks/useAgentLoop.ts | 30 ----
src/mcp/connection.ts | 2 +-
src/providers/openrouter/oauth-provider.ts | 2 +-
4 files changed, 161 insertions(+), 32 deletions(-)
create mode 100644 docs/security_design_doc.md
diff --git a/docs/security_design_doc.md b/docs/security_design_doc.md
new file mode 100644
index 0000000..c4a6e9e
--- /dev/null
+++ b/docs/security_design_doc.md
@@ -0,0 +1,159 @@
+# Security Design Document: MCP Remote Client
+
+## Executive Summary
+
+The MCP Remote Client is an example open-source web application that demonstrates how to build a client for the Model Context Protocol (MCP). It is designed as a standalone, statically-served application without a backend server, intended for educational and reference purposes rather than production use. The application enables users to connect to MCP servers and inference providers (like OpenRouter) to interact with AI models and tools.
+
+## Application Architecture
+
+### Deployment Model
+- **Static Web Application**: Deployed as static HTML/JS/CSS files
+- **No Backend Server**: All processing occurs client-side in the browser
+- **No Data Exfiltration**: No user data is sent to external analytics or backend services
+- **Local-First Design**: All data persistence uses browser localStorage
+
+### Core Components
+
+1. **MCP Connection Manager**: Handles connections to MCP servers via SSE or HTTP streaming
+2. **Inference Provider System**: Manages connections to AI inference services (OpenRouter)
+3. **Conversation Manager**: Stores and manages conversation history locally
+4. **OAuth Manager**: Handles OAuth 2.0 + PKCE flows for authentication
+
+## Authentication and Authorization
+
+### OAuth 2.0 Implementation
+
+#### MCP Server Authentication
+- **OAuth 2.0 with PKCE**: Implements authorization code flow with Proof Key for Code Exchange
+- **State Parameter**: Includes connection ID prefix (`mcp:${connectionId}`) for callback routing
+- **Code Verifier**: Generated using crypto.getRandomValues() with SHA-256 challenge
+- **Token Storage**: Access tokens stored in localStorage at `mcp_oauth_tokens_${connectionId}`
+- **Client Registration**: OAuth client information cached per server URL to minimize re-registration
+
+#### Inference Provider Authentication
+- **Dual Authentication**: Supports both API keys and OAuth flows
+- **OpenRouter OAuth**: Similar PKCE implementation with `inference:` state prefix
+- **Token Persistence**: Tokens stored in localStorage with automatic loading on app start
+- **No Refresh Tokens**: Current implementation does not support token refresh
+
+### API Key Management
+- **Local Storage**: API keys stored in browser localStorage
+- **No Transmission**: Keys are never sent to third-party services beyond the intended providers
+- **User-Controlled**: Users manually input and manage their API keys
+
+## Data Storage and Privacy
+
+### Storage Architecture
+- **localStorage**: Primary persistence mechanism for all application data
+- **No Encryption**: Data stored in plaintext (security trade-off for example application)
+- **Browser Sandbox**: Relies on browser same-origin policy for isolation
+
+### Data Types Stored
+1. **Conversation History**: Complete message history including AI responses
+2. **OAuth Tokens**: Access tokens for MCP servers and inference providers
+3. **API Keys**: User-provided API keys for inference services
+4. **Connection Configurations**: MCP server URLs and settings
+5. **OAuth Client Registrations**: Cached OAuth client credentials
+
+## External Service Integration
+
+### MCP Server Connections
+- **Transport Methods**: SSE (Server-Sent Events) and Streamable HTTP
+- **CORS Dependency**: Requires proper CORS headers from MCP servers
+- **Connection Isolation**: Each server connection runs independently
+- **Error Handling**: Fallback from HTTP streaming to SSE on connection failure
+
+### Inference Provider Integration
+- **OpenRouter**: Primary supported provider with OAuth and API key authentication
+- **Tool Calling**: Native support for function/tool calling with parameter validation
+- **Message Format**: Standard OpenAI-compatible message format
+
+## Security Controls
+
+### Input Validation
+- **URL Validation**: MCP server URLs validated before connection attempts
+- **Tool Parameter Validation**: Function parameters validated against defined schemas
+- **Message Sanitization**: React's built-in XSS protection for rendering
+
+### OAuth Security
+- **PKCE Implementation**: Proper code verifier/challenge generation
+- **State Validation**: State parameter checked for OAuth callback verification
+- **Popup Windows**: OAuth flows use popup windows to maintain app state
+- **Origin Checking**: postMessage origin validation for popup communication
+
+### Transport Security
+- **HTTPS Enforcement**: Relies on browser security for HTTPS connections
+- **No Certificate Pinning**: Standard browser certificate validation
+- **WebSocket Security**: Uses secure WebSocket connections for SSE
+
+## Known Security Considerations
+
+### Client-Side Storage Risks
+- **localStorage Exposure**: All data accessible to JavaScript code
+- **XSS Vulnerability**: Stored tokens/keys exposed if XSS attack succeeds
+- **No Encryption**: Sensitive data stored in plaintext
+
+### Authentication Risks
+- **Token Persistence**: No automatic token expiration or rotation
+- **Public Client**: OAuth uses public client flow (no client secret)
+- **Popup Blocking**: OAuth flow fails if popups are blocked
+
+### Code Execution Risks
+- **eval() Usage**: Mathematical expression evaluation uses eval() with regex filtering
+- **Tool Execution**: Executes tools based on MCP server responses
+- **No Sandboxing**: Tool responses rendered directly in UI
+
+## Security Boundaries
+
+### Trust Boundaries
+1. **Browser Sandbox**: Primary security boundary
+2. **Same-Origin Policy**: Prevents cross-origin data access
+3. **User Consent**: Users explicitly add MCP servers and API keys
+
+### Data Flow
+```
+User Input -> React App -> localStorage
+ -> OAuth Provider -> External Service
+ -> MCP Server -> Tool Execution
+```
+
+## Incident Response Considerations
+
+### Logging and Monitoring
+- **Console Logging**: Errors logged to browser console
+- **No Audit Trail**: No persistent security event logging
+- **User-Visible Errors**: Error messages displayed directly to users
+
+### Data Cleanup
+- **Manual Cleanup**: Users must manually clear localStorage
+- **Logout Function**: Clears authentication tokens but not conversation history
+- **No Automatic Expiry**: Data persists indefinitely
+
+## Compliance and Privacy
+
+### Data Residency
+- **Client-Side Only**: All data remains in user's browser
+- **No Data Collection**: No analytics or telemetry
+- **User Control**: Full user control over data persistence
+
+### GDPR Considerations
+- **No Personal Data Processing**: Application doesn't process data server-side
+- **Right to Erasure**: Users can clear localStorage at any time
+- **Data Portability**: Conversations stored in standard JSON format
+
+## Security Recommendations for Production Use
+
+While this is an example application not intended for production, organizations adapting this code should consider:
+
+1. **Encrypt localStorage**: Implement client-side encryption for sensitive data
+2. **Replace eval()**: Use a safe expression parser library
+3. **Add CSP Headers**: Implement Content Security Policy
+4. **Token Rotation**: Implement automatic token refresh and rotation
+5. **Audit Logging**: Add security event logging
+6. **Input Sanitization**: Additional validation beyond React defaults
+7. **Rate Limiting**: Prevent abuse of OAuth flows
+8. **Secure Token Storage**: Consider Web Crypto API for token encryption
+
+## Conclusion
+
+The MCP Remote Client demonstrates a functional implementation of the Model Context Protocol with OAuth authentication and local data persistence. As an example application, it prioritizes simplicity and clarity over production-grade security controls. The local-first architecture eliminates many traditional web application security concerns but introduces client-side storage risks that users should understand.
\ No newline at end of file
diff --git a/src/hooks/useAgentLoop.ts b/src/hooks/useAgentLoop.ts
index af927bd..c79302b 100644
--- a/src/hooks/useAgentLoop.ts
+++ b/src/hooks/useAgentLoop.ts
@@ -48,36 +48,6 @@ const testTools: TestTool[] = [
return weather;
},
},
- {
- type: 'function',
- function: {
- name: 'calculate',
- description: 'Perform basic arithmetic calculations',
- parameters: {
- type: 'object',
- properties: {
- expression: {
- type: 'string',
- description: 'Mathematical expression to evaluate (e.g., "2 + 2", "10 * 3")',
- },
- },
- required: ['expression'],
- },
- },
- execute: async (args) => {
- try {
- // Simple expression evaluator (basic safety check)
- const expression = args.expression.replace(/[^0-9+\-*/().\s]/g, '');
- if (expression !== args.expression) {
- throw new Error('Invalid characters in expression');
- }
- const result = eval(expression);
- return { expression: args.expression, result };
- } catch (error) {
- throw new Error(`Calculation error: ${error instanceof Error ? error.message : 'Unknown error'}`);
- }
- },
- },
{
type: 'function',
function: {
diff --git a/src/mcp/connection.ts b/src/mcp/connection.ts
index e00cadd..704c612 100644
--- a/src/mcp/connection.ts
+++ b/src/mcp/connection.ts
@@ -162,7 +162,7 @@ class MCPOAuthProvider implements OAuthClientProvider {
const oauthState: MCPOAuthState = {
codeVerifier,
state: this.generateRandomString(16),
- expiresAt: Date.now() + (10 * 60 * 1000), // 10 minutes
+ expiresAt: Date.now() + (5 * 60 * 1000), // 5 minutes
};
localStorage.setItem(`mcp_oauth_state_${this.connectionId}`, JSON.stringify(oauthState));
}
diff --git a/src/providers/openrouter/oauth-provider.ts b/src/providers/openrouter/oauth-provider.ts
index 15f8623..57982bf 100644
--- a/src/providers/openrouter/oauth-provider.ts
+++ b/src/providers/openrouter/oauth-provider.ts
@@ -184,7 +184,7 @@ export class OpenRouterOAuthProvider extends InferenceProvider {
const oauthState: OAuthState = {
codeVerifier,
state,
- expiresAt: Date.now() + (10 * 60 * 1000), // 10 minutes
+ expiresAt: Date.now() + (5 * 60 * 1000), // 5 minutes
};
localStorage.setItem('openrouter_oauth_state', JSON.stringify(oauthState));
From e53be9ec42ed6e66e8be08680ebda7d680714032 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 19 Jun 2025 16:55:47 +0100
Subject: [PATCH 26/33] Add support for in-memory MCP servers
- Add InMemoryTransport support for local servers running in the same process
- Update MCPConnectionManager to pass full connection object in callbacks for better context
- Filter out local servers from persistence (they auto-connect on startup)
- Add 'In-Memory' badge in UI to distinguish local servers
- Fix connection state management to properly update connectionsRef
- Auto-connect to available in-memory servers on startup
This enables testing MCP functionality without external servers by running
servers directly in the browser using the InMemoryTransport from the SDK.
---
src/components/MCPTab.tsx | 5 +++
src/contexts/MCPContext.tsx | 68 +++++++++++++++++++++++++++--------
src/mcp/connection.ts | 52 ++++++++++++++++++++-------
src/mcp/servers/index.ts | 10 ++++++
src/mcp/servers/test/index.ts | 24 +++++++++++++
src/types/mcp.ts | 5 +--
6 files changed, 136 insertions(+), 28 deletions(-)
create mode 100644 src/mcp/servers/index.ts
create mode 100644 src/mcp/servers/test/index.ts
diff --git a/src/components/MCPTab.tsx b/src/components/MCPTab.tsx
index c8f136d..64dbd43 100644
--- a/src/components/MCPTab.tsx
+++ b/src/components/MCPTab.tsx
@@ -222,6 +222,11 @@ export function MCPTab() {
{connection.name}
+ {connection.url === 'local' && (
+
+ In-Memory
+
+ )}
{connection.status}
diff --git a/src/contexts/MCPContext.tsx b/src/contexts/MCPContext.tsx
index ede9045..d35fce6 100644
--- a/src/contexts/MCPContext.tsx
+++ b/src/contexts/MCPContext.tsx
@@ -13,6 +13,7 @@ import type {
} from '@/types/mcp';
import type { Tool } from '@/types/inference';
import { MCPConnectionManager } from '@/mcp/connection';
+import { availableServers } from '@/mcp/servers';
const MCPContext = createContext(null);
@@ -56,24 +57,18 @@ export function MCPProvider({ children }: MCPProviderProps) {
// Function to broadcast messages to all callbacks (stable reference)
const broadcastMessage = useCallback((
- connectionId: string,
+ connection: MCPConnection,
client: any,
message: any,
direction: 'sent' | 'received',
extra?: any
) => {
- // console.log(`MCP Message [${direction}] from ${connectionId}:`, message); // Debug log
-
- // Find connection name using ref to get latest state
- const connection = connectionsRef.current.find(conn => conn.id === connectionId);
- const serverName = connection?.name || 'Unknown Server';
-
// Store message
const storedMessage: MCPMonitorMessage = {
id: uuidv4(),
timestamp: new Date(),
- connectionId,
- serverName,
+ connectionId: connection.id,
+ serverName: connection?.name || 'Unknown Server',
message,
direction,
extra,
@@ -82,7 +77,7 @@ export function MCPProvider({ children }: MCPProviderProps) {
messageCallbacksRef.current.forEach(callback => {
try {
- callback(connectionId, client, message, direction, extra);
+ callback(connection.id, client, message, direction, extra);
} catch (error) {
console.error('Error in MCP message callback:', error);
}
@@ -118,11 +113,12 @@ export function MCPProvider({ children }: MCPProviderProps) {
// Set up callback for connection state updates
manager.setConnectionUpdateCallback(() => {
- setConnections(prev =>
- prev.map(conn =>
+ setConnections(prev => {
+ connectionsRef.current = prev.map(conn =>
conn.id === connectionId ? manager.getConnection() : conn
)
- );
+ return connectionsRef.current
+ });
});
// Set up message callback
@@ -149,6 +145,50 @@ export function MCPProvider({ children }: MCPProviderProps) {
} catch (error) {
console.error('Failed to load persisted MCP connections:', error);
}
+
+ // After loading persisted connections, add local servers
+ await addLocalServers();
+ };
+
+ const addLocalServers = async () => {
+ // Check if local servers are already added
+ const hasLocalServers = connectionsRef.current.some(conn => conn.url === 'local');
+ if (hasLocalServers) {
+ return;
+ }
+
+ // Add each available local server
+ for (const serverConfig of availableServers) {
+ try {
+ const connectionId = uuidv4();
+ const manager = new MCPConnectionManager(connectionId, serverConfig);
+
+ // Set up callback for connection state updates
+ manager.setConnectionUpdateCallback(() => {
+ setConnections(prev => {
+ connectionsRef.current = prev.map(conn =>
+ conn.id === connectionId ? manager.getConnection() : conn
+ );
+ return connectionsRef.current;
+ });
+ });
+
+ // Set up message callback
+ manager.setMessageCallback(broadcastMessage);
+
+ // Add to managers map
+ setManagers(prev => new Map(prev).set(connectionId, manager));
+
+ // Add initial connection state
+ setConnections(prev => [...prev, manager.getConnection()]);
+
+ // Auto-connect local servers
+ await manager.connect();
+ console.log(`Connected to local server: ${serverConfig.name}`);
+ } catch (error) {
+ console.error(`Failed to connect to local server ${serverConfig.name}:`, error);
+ }
+ }
};
loadPersistedConnections();
@@ -157,7 +197,7 @@ export function MCPProvider({ children }: MCPProviderProps) {
const persistConnections = useCallback(() => {
try {
// Store both config and connection ID to maintain OAuth token association
- const connectionData = connections.map(conn => ({
+ const connectionData = connections.filter(conn => conn.url !== 'local').map(conn => ({
id: conn.id,
config: conn.config
}));
diff --git a/src/mcp/connection.ts b/src/mcp/connection.ts
index 704c612..56c356d 100644
--- a/src/mcp/connection.ts
+++ b/src/mcp/connection.ts
@@ -3,6 +3,7 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
+import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import {
auth,
@@ -235,7 +236,7 @@ export class MCPConnectionManager {
private healthCheckInterval?: NodeJS.Timeout;
private oauthProvider?: MCPOAuthProvider;
private onConnectionUpdate?: () => void;
- private onMessage?: (connectionId: string, client: any, message: any, direction: 'sent' | 'received', extra?: any) => void;
+ private onMessage?: (connection: MCPConnection, client: any, message: any, direction: 'sent' | 'received', extra?: any) => void;
constructor(id: string, config: MCPServerConfig) {
this.connection = {
@@ -265,7 +266,7 @@ export class MCPConnectionManager {
}
// Set callback for message monitoring
- setMessageCallback(callback: (connectionId: string, client: any, message: any, direction: 'sent' | 'received', extra?: any) => void): void {
+ setMessageCallback(callback: (connection: MCPConnection, client: any, message: any, direction: 'sent' | 'received', extra?: any) => void): void {
this.onMessage = callback;
}
@@ -286,14 +287,20 @@ export class MCPConnectionManager {
// Clear any existing connections
await this.disconnect();
- try {
- await this.tryStreamableHttp();
- this.connection.transport = 'streamable-http';
- } catch (error) {
- // TODO: jerome - if this is a TypeError: failed to fetch, then there is likely a CORS (or
- // Access-Control-Expose-Headers) issue with the server.
- await this.trySSE();
- this.connection.transport = 'sse';
+ // Check if this is a local server
+ if (this.connection.url === 'local') {
+ await this.tryInMemory();
+ this.connection.transport = 'inmemory';
+ } else {
+ try {
+ await this.tryStreamableHttp();
+ this.connection.transport = 'streamable-http';
+ } catch (error) {
+ // TODO: jerome - if this is a TypeError: failed to fetch, then there is likely a CORS (or
+ // Access-Control-Expose-Headers) issue with the server.
+ await this.trySSE();
+ this.connection.transport = 'sse';
+ }
}
// Initialize client capabilities
@@ -435,6 +442,27 @@ export class MCPConnectionManager {
}
}
+ private async tryInMemory(): Promise {
+ try {
+ if (!this.connection.config.localServer) {
+ throw new Error('Local server function not provided');
+ }
+
+ // Create linked transport pair
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+
+ // Create and connect the server
+ const server = this.connection.config.localServer();
+ await server.connect(serverTransport);
+
+ // Initialize the client with its transport
+ await this.initializeClient(clientTransport);
+ } catch (error) {
+ console.log('InMemory connection failed:', error);
+ throw error;
+ }
+ }
+
private async initializeClient(transport: Transport): Promise {
try {
const debugTransport = new DebugTransport(transport);
@@ -453,13 +481,13 @@ export class MCPConnectionManager {
// Set up message callbacks to broadcast to UI after client is created
debugTransport.onsendmessage_ = async (message, options) => {
if (this.onMessage && this.client) {
- this.onMessage(this.connection.id, this.client, message, 'sent', { options });
+ this.onMessage(this.connection, this.client, message, 'sent', { options });
}
};
debugTransport.onreceivemessage_ = (message, extra) => {
if (this.onMessage && this.client) {
- this.onMessage(this.connection.id, this.client, message, 'received', extra);
+ this.onMessage(this.connection, this.client, message, 'received', extra);
}
};
diff --git a/src/mcp/servers/index.ts b/src/mcp/servers/index.ts
new file mode 100644
index 0000000..4976b9b
--- /dev/null
+++ b/src/mcp/servers/index.ts
@@ -0,0 +1,10 @@
+import { createServer as createTestServer } from "./test";
+import type { MCPServerConfig } from "@/types/mcp";
+
+export const availableServers: MCPServerConfig[] = [
+ {
+ name: "In-Memory Test Server",
+ url: "local",
+ localServer: createTestServer
+ }
+];
\ No newline at end of file
diff --git a/src/mcp/servers/test/index.ts b/src/mcp/servers/test/index.ts
new file mode 100644
index 0000000..6c6d4ce
--- /dev/null
+++ b/src/mcp/servers/test/index.ts
@@ -0,0 +1,24 @@
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { z } from "zod";
+
+
+export function createServer() {
+ // Create an MCP server
+ const server = new McpServer({
+ name: "demo-server",
+ version: "1.0.0"
+ });
+
+ // Add an addition tool
+ server.registerTool("add",
+ {
+ title: "Addition Tool",
+ description: "Add two numbers",
+ inputSchema: { a: z.number(), b: z.number() },
+ },
+ async ({ a, b }) => ({
+ content: [{ type: "text", text: String(a + b) }]
+ })
+ );
+ return server;
+}
\ No newline at end of file
diff --git a/src/types/mcp.ts b/src/types/mcp.ts
index 1eab0d3..fc28b92 100644
--- a/src/types/mcp.ts
+++ b/src/types/mcp.ts
@@ -8,7 +8,7 @@ import type { TransportSendOptions } from '@modelcontextprotocol/sdk/shared/tran
export interface MCPServerConfig {
name: string; // User-provided display name
- url: string; // Server endpoint URL
+ url: string; // Server endpoint URL (or 'local' for in-memory servers)
authType?: 'none' | 'oauth'; // Default: none
oauthConfig?: {
clientId?: string;
@@ -18,6 +18,7 @@ export interface MCPServerConfig {
redirectUri?: string; // Override default redirect URI
};
maxReconnectAttempts?: number; // Default: 5
+ localServer?: () => any; // Function to create the local server instance (when url === 'local')
}
export interface MCPMessage {
@@ -73,7 +74,7 @@ export interface MCPConnection {
url: string; // Server URL
status: 'connecting' | 'connected' | 'failed' | 'disconnected';
client?: Client; // MCP SDK client instance
- transport?: 'sse' | 'streamable-http';
+ transport?: 'sse' | 'streamable-http' | 'inmemory';
authType?: 'none' | 'oauth';
// Available capabilities
From e746c42f8dc9d7241ea5700fbc8fd797d389456f Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 19 Jun 2025 17:12:50 +0100
Subject: [PATCH 27/33] Add documentation for creating in-memory MCP servers
- Explain how to create and register in-memory servers
- Provide examples using the Server class and tool() method
- Include best practices and debugging tips
- Document limitations of in-memory servers
- Add complete calculator server example
---
docs/adding_local_servers.md | 249 +++++++++++++++++++++++++++++++++++
1 file changed, 249 insertions(+)
create mode 100644 docs/adding_local_servers.md
diff --git a/docs/adding_local_servers.md b/docs/adding_local_servers.md
new file mode 100644
index 0000000..843f285
--- /dev/null
+++ b/docs/adding_local_servers.md
@@ -0,0 +1,249 @@
+# Adding In-Memory MCP Servers
+
+This guide explains how to add in-memory MCP servers that run within the browser, using the InMemoryTransport from the MCP SDK.
+
+## Overview
+
+In-memory servers are useful for:
+- Testing MCP functionality without external servers
+- Providing demo capabilities
+- Development and debugging
+- Offline functionality
+
+These servers run in the same JavaScript process as the client and communicate via the InMemoryTransport, eliminating network overhead and CORS issues.
+
+## Creating an In-Memory Server
+
+### 1. Create Your Server Implementation
+
+Create a new file in `src/mcp/servers/your-server/index.ts`:
+
+```typescript
+import { Server } from "@modelcontextprotocol/sdk/server/index.js";
+import { z } from "zod";
+
+export function createServer() {
+ // Create an MCP server instance
+ const server = new Server({
+ name: "your-server-name",
+ version: "1.0.0"
+ }, {
+ capabilities: {
+ tools: {} // Enable tools capability
+ }
+ });
+
+ // Register tools using the McpFunction helper
+ server.tool(
+ "tool_name",
+ "Tool description",
+ {
+ // Zod schema for input validation
+ param1: z.string().describe("Parameter description"),
+ param2: z.number().optional()
+ },
+ async ({ param1, param2 }) => {
+ // Tool implementation
+ return {
+ content: [{
+ type: "text",
+ text: `Result: ${param1}`
+ }]
+ };
+ }
+ );
+
+ return server;
+}
+```
+
+### 2. Register Your Server
+
+Add your server to the available servers list in `src/mcp/servers/index.ts`:
+
+```typescript
+import { createServer as createTestServer } from "./test";
+import { createServer as createYourServer } from "./your-server";
+import type { MCPServerConfig } from "@/types/mcp";
+
+export const availableServers: MCPServerConfig[] = [
+ {
+ name: "In-Memory Test Server",
+ url: "local",
+ localServer: createTestServer
+ },
+ {
+ name: "Your Server Name",
+ url: "local",
+ localServer: createYourServer
+ }
+];
+```
+
+## Server Implementation Details
+
+### Tool Registration
+
+The MCP SDK provides a convenient `tool()` method for registering tools:
+
+```typescript
+server.tool(
+ "tool_name", // Tool identifier
+ "Tool description", // Human-readable description
+ { // Zod schema for parameters
+ param: z.string()
+ },
+ async (args) => { // Implementation function
+ // Tool logic here
+ return {
+ content: [{
+ type: "text",
+ text: "Result"
+ }]
+ };
+ }
+);
+```
+
+### Available Content Types
+
+Tools can return different content types:
+
+```typescript
+// Text content
+return {
+ content: [{
+ type: "text",
+ text: "Hello, world!"
+ }]
+};
+
+// Error content
+return {
+ content: [{
+ type: "text",
+ text: "Error: Something went wrong"
+ }],
+ isError: true
+};
+
+// Multiple content blocks
+return {
+ content: [
+ { type: "text", text: "Line 1" },
+ { type: "text", text: "Line 2" }
+ ]
+};
+```
+
+### Resources and Prompts
+
+You can also implement resources and prompts:
+
+```typescript
+// Resources
+server.resource(
+ "resource_uri",
+ "Resource name",
+ "Resource description",
+ async () => ({
+ content: [{
+ type: "text",
+ text: "Resource content"
+ }]
+ })
+);
+
+// Prompts
+server.prompt(
+ "prompt_name",
+ "Prompt description",
+ {
+ param: z.string()
+ },
+ async ({ param }) => ({
+ messages: [{
+ role: "user",
+ content: { type: "text", text: `Prompt with ${param}` }
+ }]
+ })
+);
+```
+
+## Best Practices
+
+1. **Naming**: Use descriptive names for your servers and tools
+2. **Error Handling**: Always handle errors gracefully and return meaningful error messages
+3. **Validation**: Use Zod schemas to validate input parameters
+4. **Documentation**: Include clear descriptions for tools and parameters
+5. **Testing**: Test your server implementation before adding it to the production list
+
+## Example: Calculator Server
+
+Here's a complete example of a calculator server:
+
+```typescript
+import { Server } from "@modelcontextprotocol/sdk/server/index.js";
+import { z } from "zod";
+
+export function createServer() {
+ const server = new Server({
+ name: "calculator",
+ version: "1.0.0"
+ }, {
+ capabilities: {
+ tools: {}
+ }
+ });
+
+ server.tool(
+ "add",
+ "Add two numbers",
+ {
+ a: z.number().describe("First number"),
+ b: z.number().describe("Second number")
+ },
+ async ({ a, b }) => ({
+ content: [{
+ type: "text",
+ text: `${a} + ${b} = ${a + b}`
+ }]
+ })
+ );
+
+ server.tool(
+ "multiply",
+ "Multiply two numbers",
+ {
+ a: z.number().describe("First number"),
+ b: z.number().describe("Second number")
+ },
+ async ({ a, b }) => ({
+ content: [{
+ type: "text",
+ text: `${a} × ${b} = ${a * b}`
+ }]
+ })
+ );
+
+ return server;
+}
+```
+
+## Debugging
+
+In-memory servers will automatically connect when the application starts. You can see their status in the MCP tab of the UI, where they'll be marked with an "In-Memory" badge.
+
+To debug your server:
+1. Check the browser console for connection errors
+2. Use the MCP message monitor to see tool calls and responses
+3. Add console.log statements in your tool implementations
+
+## Limitations
+
+- In-memory servers are cleared when the page refreshes
+- They cannot persist data between sessions
+- They run in the browser's JavaScript environment, so they cannot access filesystem or system resources
+- Performance is limited by the browser's capabilities
+
+For production use cases requiring persistence or system access, use external MCP servers instead.
\ No newline at end of file
From 496a0a382b413fba4a569e2ac7a1d78ea34f8df5 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Tue, 24 Jun 2025 12:47:57 -0700
Subject: [PATCH 28/33] Adding WIP notice
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index 06701b3..5238bdd 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
+# NOTE: this is a work in progress
+
# Example Remote MCP Client
A React TypeScript application for connecting to multiple MCP (Model Context Protocol) servers and providing a conversational interface with tool calling capabilities.
From 6b148647b90c059a0740864c908ccf484de62a5f Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 3 Jul 2025 11:32:39 +0100
Subject: [PATCH 29/33] Add GitHub Pages deployment workflow
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Create GitHub Actions workflow for automatic deployment on main branch pushes
- Configure Vite base path for GitHub Pages hosting
- Set up build and deploy pipeline with proper permissions
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
.github/workflows/deploy.yml | 49 ++++++++++++++++++++++++++++++++++++
vite.config.ts | 2 +-
2 files changed, 50 insertions(+), 1 deletion(-)
create mode 100644 .github/workflows/deploy.yml
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..6b3012e
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,49 @@
+name: Deploy to GitHub Pages
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '18'
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build
+ run: npm run build
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: ./dist
+
+ deploy:
+ needs: build
+ runs-on: ubuntu-latest
+
+ permissions:
+ pages: write
+ id-token: write
+
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
index f4734f6..babf8ce 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -7,7 +7,7 @@ export default defineConfig({
plugins: [react()],
// Set base to '/' for local testing, or to your repo name for GitHub Pages
// e.g., base: '/example-remote-client/' for https://username.github.io/example-remote-client/
- base: '/',
+ base: process.env.NODE_ENV === 'production' ? '/example-remote-client/' : '/',
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
From 0006f25269c0d014d110f4e0994fed4be9e517d6 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Thu, 3 Jul 2025 11:47:45 +0100
Subject: [PATCH 30/33] Fix TypeScript build errors
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add @types/node dependency for NodeJS namespace types
- Remove unsupported 'title' property from MCP tool registration
- Add Node.js types to tsconfig.json for proper type resolution
- Ensure build succeeds for GitHub Pages deployment
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
package-lock.json | 18 ++++++++++++++++++
package.json | 15 ++++++++-------
src/mcp/servers/test/index.ts | 1 -
tsconfig.json | 1 +
4 files changed, 27 insertions(+), 8 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index cee0e06..677035e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"uuid": "^10.0.0"
},
"devDependencies": {
+ "@types/node": "^24.0.10",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@types/uuid": "^10.0.0",
@@ -1453,6 +1454,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/node": {
+ "version": "24.0.10",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
+ "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.8.0"
+ }
+ },
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
@@ -5673,6 +5684,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/undici-types": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
+ "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
diff --git a/package.json b/package.json
index 1217b79..ee85b5c 100644
--- a/package.json
+++ b/package.json
@@ -12,20 +12,22 @@
"test:ui": "vitest --ui"
},
"dependencies": {
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
"@modelcontextprotocol/sdk": "^1.0.0",
- "uuid": "^10.0.0",
"clsx": "^2.0.0",
- "lucide-react": "^0.344.0"
+ "lucide-react": "^0.344.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "uuid": "^10.0.0"
},
"devDependencies": {
+ "@types/node": "^24.0.10",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.2.1",
+ "@vitest/ui": "^1.2.0",
"autoprefixer": "^10.4.17",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
@@ -34,7 +36,6 @@
"tailwindcss": "^3.4.1",
"typescript": "^5.2.2",
"vite": "^5.1.0",
- "vitest": "^1.2.0",
- "@vitest/ui": "^1.2.0"
+ "vitest": "^1.2.0"
}
-}
\ No newline at end of file
+}
diff --git a/src/mcp/servers/test/index.ts b/src/mcp/servers/test/index.ts
index 6c6d4ce..77fef03 100644
--- a/src/mcp/servers/test/index.ts
+++ b/src/mcp/servers/test/index.ts
@@ -12,7 +12,6 @@ export function createServer() {
// Add an addition tool
server.registerTool("add",
{
- title: "Addition Tool",
description: "Add two numbers",
inputSchema: { a: z.number(), b: z.number() },
},
diff --git a/tsconfig.json b/tsconfig.json
index 416e717..b09e335 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,6 +3,7 @@
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "types": ["node"],
"module": "ESNext",
"skipLibCheck": true,
From ccc21b1e6131627da445d50423db222888cfe587 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Fri, 4 Jul 2025 15:53:50 +0100
Subject: [PATCH 31/33] Update Vite config for root domain hosting
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Set base path to '/' for modelcontextprotocol.github.io root domain
- Remove conditional path logic for cleaner configuration
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
vite.config.ts | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/vite.config.ts b/vite.config.ts
index babf8ce..db91dc5 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -5,9 +5,8 @@ import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
- // Set base to '/' for local testing, or to your repo name for GitHub Pages
- // e.g., base: '/example-remote-client/' for https://username.github.io/example-remote-client/
- base: process.env.NODE_ENV === 'production' ? '/example-remote-client/' : '/',
+ // Set base to '/' for root domain hosting at modelcontextprotocol.github.io
+ base: '/',
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
From 6403c23949e44e44b02f15eba3fc895d1ca356c7 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Fri, 4 Jul 2025 15:55:30 +0100
Subject: [PATCH 32/33] Revert Vite config to subdirectory hosting
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Restore conditional base path for /example-remote-client/ subdirectory
- Keep current GitHub Pages setup at subdirectory URL
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
vite.config.ts | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/vite.config.ts b/vite.config.ts
index db91dc5..babf8ce 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -5,8 +5,9 @@ import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
- // Set base to '/' for root domain hosting at modelcontextprotocol.github.io
- base: '/',
+ // Set base to '/' for local testing, or to your repo name for GitHub Pages
+ // e.g., base: '/example-remote-client/' for https://username.github.io/example-remote-client/
+ base: process.env.NODE_ENV === 'production' ? '/example-remote-client/' : '/',
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
From f1736b32b5a7fbbe587956e4c7f6a3d2c2b0a32e Mon Sep 17 00:00:00 2001
From: Jerome
Date: Fri, 11 Jul 2025 09:16:19 +0100
Subject: [PATCH 33/33] Updated vite base path because we're using a custom
domain for the github pages deployment now
---
vite.config.ts | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/vite.config.ts b/vite.config.ts
index babf8ce..4f035b9 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -5,9 +5,7 @@ import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
- // Set base to '/' for local testing, or to your repo name for GitHub Pages
- // e.g., base: '/example-remote-client/' for https://username.github.io/example-remote-client/
- base: process.env.NODE_ENV === 'production' ? '/example-remote-client/' : '/',
+ base: '/',
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),