@portkey-ai/hoot 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,277 +1,87 @@
1
- # 🦉 Hoot - MCP Testing Tool
1
+ # 🦉 hoot
2
+ > **⚠️ early beta** - things might break. if they do, [open an issue](https://github.com/Portkey-AI/hoot/issues).
3
+ >
4
+ > **🤝 contributions welcome** - see something that could be better? PRs are appreciated!
2
5
 
3
- **Postman for MCP Servers** - A fast, lightweight tool for testing Model Context Protocol servers.
6
+ testing tool for MCP servers. like postman but for MCP.
4
7
 
5
- ## ✨ Features
8
+ ## why
6
9
 
7
- - 🚀 **Fast & Lightweight** - Browser-based UI with Node.js backend
8
- - 🔐 **Full OAuth 2.1 Support** - PKCE, automatic token refresh, secure storage
9
- - 🌐 **No CORS Issues** - Backend relay architecture eliminates CORS problems
10
- - 🎨 **Beautiful UI** - Ayu Mirage theme, smooth animations
11
- - 📋 **Tool Testing** - Execute tools, view results, copy responses
12
- - 💾 **Persistent Storage** - Servers, tools, and history cached locally
13
- - 🐛 **Dev Logger** - Download console logs for debugging
14
- - ⚡ **Real-time Feedback** - Live execution timer, toast notifications
15
- - 🎯 **Smart UX** - Empty states, error boundaries, loading skeletons
10
+ needed a quick way to test MCP servers without spinning up a whole AI chat interface.
16
11
 
17
- ## 🚀 Quick Start
12
+ ## install
18
13
 
19
- ### Run with npx (Recommended)
20
14
  ```bash
21
15
  npx -y @portkey-ai/hoot
22
16
  ```
23
17
 
24
- That's it! This command will:
25
- - ✅ Install Hoot if not already installed
26
- - ✅ Start the backend server on `http://localhost:3002`
27
- - ✅ Start the frontend UI on `http://localhost:5173`
28
- - ✅ Open your browser automatically
18
+ that's it. opens on localhost:8009
29
19
 
30
- Press `Ctrl+C` to stop both servers.
31
-
32
- ### Install Globally (Alternative)
20
+ or install globally if you want:
33
21
  ```bash
34
22
  npm install -g @portkey-ai/hoot
35
23
  hoot
36
24
  ```
37
25
 
38
- ### Development Setup
39
- If you want to contribute or develop locally:
40
-
41
- ```bash
42
- # Clone the repository
43
- git clone https://github.com/yourusername/hoot.git
44
- cd hoot
45
-
46
- # Install dependencies
47
- npm install
48
-
49
- # Start both backend and frontend
50
- npm run dev:full
51
-
52
- # Or start them separately
53
- npm run backend # Terminal 1
54
- npm run dev # Terminal 2
55
- ```
56
-
57
- This starts:
58
- - **Backend MCP Server** on `http://localhost:3002` (handles MCP connections)
59
- - **Hoot UI** on `http://localhost:5173` (Vite dev server)
60
-
61
- ## 📖 Usage
26
+ ## what works
62
27
 
63
- 1. **Start Hoot** - Run `npx -y @portkey-ai/hoot`
64
- 2. **Add Server** - Click "+ Add Server"
65
- 3. **Configure**:
66
- - Name: "My MCP Server"
67
- - Transport: HTTP or SSE
68
- - URL: https://your-mcp-server.com
69
- - Auth: None / Headers / OAuth
70
- 4. **Connect** - Server appears in sidebar with tool count
71
- 5. **Select Tool** - Click any tool to test it
72
- 6. **Execute** - Fill params, click "EXECUTE TOOL"
73
- 7. **View Results** - Response, Raw JSON, or Request tabs
28
+ - connect to MCP servers (http/sse)
29
+ - see what tools they have
30
+ - execute tools with params
31
+ - view responses
32
+ - oauth 2.1 if your server needs it
33
+ - copy stuff to clipboard
74
34
 
75
- ## 🏗️ Architecture
35
+ ## how it works
76
36
 
77
- Hoot uses a **backend relay architecture** to eliminate CORS issues:
37
+ runs a node.js backend that connects to MCP servers (because CORS is annoying). react frontend talks to the backend over localhost.
78
38
 
79
39
  ```
80
- Browser App (React) Backend Server (Node.js) MCP Servers
81
- (UI) (MCP Client) (External)
40
+ browserbackendmcp servers
82
41
  ```
83
42
 
84
- - **Frontend**: React UI running in browser
85
- - **Backend**: Node.js server with MCP SDK (handles actual connections)
86
- - **Communication**: REST API over localhost (no CORS issues)
43
+ no cors issues. backend handles oauth tokens in sqlite.
87
44
 
88
- See [docs/BACKEND_ARCHITECTURE.md](./docs/BACKEND_ARCHITECTURE.md) for detailed architecture documentation.
45
+ ### persistence
89
46
 
90
- ## 🌐 No More CORS Issues!
47
+ - **server configs & tools**: saved in browser localStorage (survives page refreshes)
48
+ - **oauth tokens**: stored in `~/.hoot/hoot-mcp.db` (persists across npx runs)
91
49
 
92
- The backend relay architecture completely eliminates CORS issues. The Node.js backend acts as the MCP client and communicates server-to-server with MCP servers, while the browser UI simply relays requests through the local backend.
50
+ your servers stay configured between sessions, even when running with `npx`!
93
51
 
94
- **Benefits**:
95
- - ✅ Works with any MCP server (no CORS configuration needed)
96
- - ✅ More secure (credentials stay on backend)
97
- - ✅ Better performance (persistent connections)
98
- - ✅ Full OAuth 2.1 support maintained
99
-
100
- **Old CORS Proxy (deprecated)**: The old proxy method is still available via `npm run dev:with-proxy`, but the backend relay is now the recommended approach.
101
-
102
- **See [docs/BACKEND_ARCHITECTURE.md](./docs/BACKEND_ARCHITECTURE.md) for detailed architecture documentation.**
103
-
104
- ## 🔐 OAuth 2.1 Support
105
-
106
- Hoot supports full OAuth 2.1 authorization flow:
107
-
108
- - ✅ Authorization redirect with PKCE
109
- - ✅ Automatic token exchange
110
- - ✅ Token refresh
111
- - ✅ Secure storage (SQLite database)
112
- - ✅ Multiple servers with different auth
113
-
114
- **See [docs/AUTHENTICATION.md](./docs/AUTHENTICATION.md) for details.**
115
-
116
- ## 🎨 UI Features
117
-
118
- ### Toasts
119
- - Connection errors
120
- - Execution errors
121
- - Proxy status changes
122
- - Copy failures
123
-
124
- ### Empty States
125
- - No servers: "Add your first server"
126
- - No tools: "Server doesn't expose tools"
127
- - No tool selected: "Select a tool to test"
128
-
129
- ### Live Feedback
130
- - **Execute button timer**: Shows elapsed time during execution
131
- - **Spinning icons**: Refresh actions
132
- - **Copy buttons**: One-click copy with checkmark feedback
133
- - **Status dots**: Green (connected) / Gray (disconnected)
134
-
135
- ### Dev Logger
136
- ```javascript
137
- // In browser console:
138
- hootLogger.download() // Download logs to file
139
- hootLogger.get() // View in console
140
- hootLogger.clear() // Clear logs
141
- hootLogger.count() // Get log count
142
- ```
143
-
144
- ## 📂 Project Structure
145
-
146
- ```
147
- hoot/
148
- ├── bin/
149
- │ └── hoot.js # CLI entry point
150
- ├── src/
151
- │ ├── components/ # React components
152
- │ │ ├── ServerSidebar.tsx
153
- │ │ ├── ToolsSidebar.tsx
154
- │ │ ├── MainArea.tsx
155
- │ │ └── ...
156
- │ ├── stores/ # Zustand state management
157
- │ │ ├── appStore.ts
158
- │ │ └── toastStore.ts
159
- │ ├── hooks/ # Custom React hooks
160
- │ │ ├── useMCP.ts
161
- │ │ └── useAutoReconnect.ts
162
- │ ├── lib/ # Core libraries
163
- │ │ ├── mcpClient.ts # MCP SDK wrapper
164
- │ │ ├── backendClient.ts # Backend API client
165
- │ │ ├── oauthProvider.ts # OAuth implementation
166
- │ │ └── logger.ts # Dev logger
167
- │ └── types/ # TypeScript types
168
- ├── docs/ # Documentation
169
- │ ├── ARCHITECTURE.md
170
- │ ├── AUTHENTICATION.md
171
- │ ├── BACKEND_ARCHITECTURE.md
172
- │ └── ...
173
- ├── mcp-backend-server.js # Backend MCP relay server
174
- └── package.json
175
- ```
176
-
177
- ## 📚 Documentation
178
-
179
- - [Architecture Overview](./docs/ARCHITECTURE.md)
180
- - [Backend Architecture](./docs/BACKEND_ARCHITECTURE.md)
181
- - [Authentication & OAuth](./docs/AUTHENTICATION.md)
182
- - [Design System](./docs/DESIGN_HOOT.md)
183
- - [Quick Start Guide](./docs/QUICKSTART.md)
184
- - [Troubleshooting](./docs/TROUBLESHOOTING.md)
185
- - [Full Documentation Index](./docs/README.md)
186
-
187
- ## 🛠️ npm Scripts
52
+ ## running from source
188
53
 
189
54
  ```bash
190
- npx -y @portkey-ai/hoot # Run Hoot (recommended)
191
- npm start # Start both backend + frontend
192
- npm run dev # Start Hoot UI only
193
- npm run backend # Start backend server only
194
- npm run dev:full # Start both (concurrently)
195
- npm run build # Build for production
196
- npm run preview # Preview production build
55
+ git clone <repo>
56
+ npm install
57
+ npm run dev:full
197
58
  ```
198
59
 
199
- ## 🔧 Tech Stack
60
+ backend runs on 8008, frontend on 8009.
200
61
 
201
- - **React 19** - UI framework
202
- - **TypeScript** - Type safety
203
- - **Vite** - Build tool
204
- - **Zustand** - State management
205
- - **MCP SDK** - Model Context Protocol
206
- - **Express** - Proxy server
207
- - **Lucide React** - Icons
62
+ ## debugging
208
63
 
209
- ## 📊 Browser Support
64
+ there's a logger in the console:
210
65
 
211
- - Chrome/Edge 90+
212
- - Firefox 88+
213
- - Safari 14+
214
-
215
- ## 🐛 Debugging
216
-
217
- ### Enable Dev Logger
218
- Automatically enabled in development. Access via browser console:
219
66
  ```javascript
220
- hootLogger.download()
221
- ```
222
-
223
- ### Check Proxy Status
224
- ```bash
225
- curl http://localhost:3001/health
67
+ hootLogger.download() // get logs
226
68
  ```
227
69
 
228
- ### View Console Logs
229
- All MCP operations are logged with emojis:
230
- - 🦉 Proxy server
231
- - 🔧 Transport creation
232
- - 🔌 Connections
233
- - 🔐 OAuth flows
234
- - ✓ Success
235
- - ❌ Errors
70
+ ## what's missing
236
71
 
237
- ## 📝 Roadmap
72
+ - resources (coming)
73
+ - prompts (coming)
74
+ - keyboard shortcuts (maybe)
75
+ - tests (oops)
238
76
 
239
- ### v0.2 (Current)
240
- - [x] Full OAuth 2.1 support
241
- - [x] CORS proxy
242
- - [x] UI polish (toasts, empty states, copy buttons)
243
- - [x] Live execution timer
244
- - [x] Dev logger
77
+ ## tech
245
78
 
246
- ### v0.3 (Planned)
247
- - [ ] Resource testing UI
248
- - [ ] Prompt testing UI
249
- - [ ] Keyboard shortcuts (Cmd+K, Cmd+E)
250
- - [ ] Browser extension (CORS bypass)
79
+ react 19, typescript, vite, zustand, express, MCP SDK
251
80
 
252
- ### v1.0 (Future)
253
- - [ ] Electron app (stdio transport support)
254
- - [ ] Encrypted credential storage
255
- - [ ] Advanced testing features
256
-
257
- ## 🤝 Contributing
258
-
259
- Contributions welcome! Please:
260
- 1. Fork the repo
261
- 2. Create a feature branch
262
- 3. Make your changes
263
- 4. Submit a pull request
264
-
265
- ## 📄 License
81
+ ## license
266
82
 
267
83
  MIT
268
84
 
269
- ## 🙏 Acknowledgments
270
-
271
- - **MCP SDK** - [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk)
272
- - **Ayu Theme** - Color palette inspiration
273
- - **Lucide** - Beautiful icons
274
-
275
85
  ---
276
86
 
277
- Made with 🦉 by developers, for developers.
87
+ made this because i was tired of curl-ing MCP servers. hope it helps.
package/bin/hoot.js CHANGED
@@ -42,7 +42,7 @@ process.on('SIGINT', cleanup);
42
42
  process.on('SIGTERM', cleanup);
43
43
 
44
44
  // Start backend server
45
- console.log('📡 Starting backend server on port 3002...');
45
+ console.log('📡 Starting backend server on port 8008...');
46
46
  const backend = spawn('node', [join(rootDir, 'mcp-backend-server.js')], {
47
47
  cwd: rootDir,
48
48
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -78,7 +78,7 @@ async function waitForBackend() {
78
78
  const maxAttempts = 30; // 30 seconds max
79
79
  for (let i = 0; i < maxAttempts; i++) {
80
80
  try {
81
- const response = await fetch('http://localhost:3002/health');
81
+ const response = await fetch('http://localhost:8008/health');
82
82
  if (response.ok) {
83
83
  return true;
84
84
  }
@@ -107,11 +107,11 @@ setTimeout(async () => {
107
107
  const srcExists = existsSync(join(rootDir, 'src'));
108
108
  const mode = srcExists ? 'development' : 'production';
109
109
 
110
- console.log(`🌐 Starting frontend in ${mode} mode on port 5173...`);
110
+ console.log(`🌐 Starting frontend in ${mode} mode on port 8009...`);
111
111
 
112
112
  // Start vite - use dev mode if src exists, preview mode if using built dist/
113
113
  const viteCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
114
- const viteArgs = srcExists ? ['vite', '--open'] : ['vite', 'preview', '--open', '--port', '5173'];
114
+ const viteArgs = srcExists ? ['vite', '--open'] : ['vite', 'preview', '--open', '--port', '8009'];
115
115
 
116
116
  const frontend = spawn(viteCommand, viteArgs, {
117
117
  cwd: rootDir,
@@ -149,12 +149,12 @@ setTimeout(async () => {
149
149
 
150
150
  // Wait a bit for frontend to fully start, then show success message
151
151
  setTimeout(() => {
152
- const port = srcExists ? 5173 : 4173;
152
+ const port = srcExists ? 8009 : 8010;
153
153
  console.log(`
154
154
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
155
155
  ✅ Hoot is running!
156
156
 
157
- Backend: http://localhost:3002
157
+ Backend: http://localhost:8008
158
158
  Frontend: http://localhost:${port}
159
159
 
160
160
  Press Ctrl+C to stop
@@ -0,0 +1 @@
1
+ @import"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap";.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:var(--spacing-3xl) var(--spacing-2xl);text-align:center;min-height:300px}.empty-state-icon{display:inline-flex;align-items:center;justify-content:center;width:96px;height:96px;border-radius:50%;background:var(--bg-tertiary);color:var(--text-tertiary);margin-bottom:var(--spacing-xl);animation:fadeIn .4s ease-out}.empty-state-title{font-size:20px;font-weight:700;color:var(--text-white);margin-bottom:var(--spacing-sm);letter-spacing:-.3px}.empty-state-description{font-size:14px;color:var(--text-secondary);line-height:1.6;max-width:400px;margin-bottom:var(--spacing-xl)}.empty-state .btn{margin-top:var(--spacing-md)}@keyframes fadeIn{0%{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}.server-sidebar{width:260px;background:var(--bg-secondary);border-right:1px solid var(--border-color);display:flex;flex-direction:column;transform:translateZ(0)}.sidebar-header{padding:var(--spacing-2xl);border-bottom:1px solid var(--border-color);background:var(--bg-primary)}.logo{display:flex;align-items:center;gap:var(--spacing-md);margin-bottom:var(--spacing-sm)}.logo-icon{font-size:28px;line-height:1;animation:pulse 3s ease-in-out infinite}.logo h1{font-size:24px;font-weight:700;color:var(--text-white);letter-spacing:-.5px}.tagline{font-size:11px;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:1px;margin-bottom:var(--spacing-md);font-weight:500}.add-server-btn{width:100%;padding:12px 16px;background:linear-gradient(135deg,var(--blue-500) 0%,var(--blue-600) 100%);color:var(--bg-primary);border:none;border-radius:var(--radius-md);cursor:pointer;font-size:14px;font-weight:600;transition:all var(--transition-fast);display:flex;align-items:center;justify-content:center;gap:var(--spacing-sm);box-shadow:0 2px 8px #5ccfe633}.add-server-btn:hover{transform:translateY(-1px);box-shadow:0 4px 12px #5ccfe64d}.add-server-btn:active{transform:translateY(0)}.servers-list{flex:1;overflow-y:auto;padding:var(--spacing-md);-webkit-overflow-scrolling:touch}.server-item{padding:var(--spacing-md);margin-bottom:var(--spacing-sm);background:var(--bg-tertiary);border-radius:var(--radius-md);cursor:pointer;transition:all var(--transition-fast);will-change:background;border:1px solid transparent}.server-item:hover{background:var(--bg-hover);border-color:var(--border-bright)}.server-item.active{background:var(--bg-active);border-color:var(--blue-500);box-shadow:0 0 0 1px var(--blue-500) inset}.server-header{display:flex;align-items:center;gap:var(--spacing-sm);margin-bottom:var(--spacing-sm)}.server-footer{display:flex;align-items:center;justify-content:space-between}.server-actions{display:flex;gap:4px;opacity:0;transition:opacity var(--transition-fast)}.server-item:hover .server-actions{opacity:1}.action-btn{background:var(--bg-hover);border:1px solid var(--border-color);border-radius:var(--radius-sm);width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all var(--transition-fast);padding:0;color:var(--text-secondary)}.action-btn:hover{transform:scale(1.05)}.edit-btn:hover{background:var(--blue-500);border-color:var(--blue-500);color:var(--bg-primary)}.delete-btn:hover{background:var(--red-500);border-color:var(--red-500);color:var(--bg-primary)}.status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;position:relative}.status-dot.connected{background:var(--green-500);box-shadow:0 0 8px var(--green-500)}.status-dot.connected:after{content:"";position:absolute;inset:-2px;border-radius:50%;border:1px solid var(--green-500);opacity:.3;animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.status-dot.disconnected{background:var(--red-500);opacity:.6}.server-name{font-weight:500;font-size:14px;color:var(--text-white);flex-grow:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tool-count{background:var(--bg-hover);padding:2px 8px;border-radius:10px;font-size:11px;color:var(--orange-500);font-weight:600;font-family:var(--font-mono)}.server-meta{display:flex;align-items:center;gap:var(--spacing-sm)}.transport-badge{font-size:10px;padding:2px 7px;border-radius:10px;font-family:var(--font-mono);font-weight:600;letter-spacing:.3px;display:inline-flex;align-items:center;gap:4px;line-height:1}.transport-badge[data-transport=http]{background:#5ccfe626;color:var(--ayu-accent-cyan);border:1px solid rgba(92,207,230,.3)}.transport-badge[data-transport=sse]{background:#73d0ff26;color:var(--ayu-accent-blue);border:1px solid rgba(115,208,255,.3)}.transport-badge[data-transport=stdio]{background:#bae67e26;color:var(--ayu-accent-green);border:1px solid rgba(186,230,126,.3)}.auth-badge{font-size:10px;padding:2px 7px;border-radius:10px;font-family:var(--font-mono);font-weight:600;letter-spacing:.3px;display:inline-flex;align-items:center;gap:4px;line-height:1}.auth-badge[data-auth-type=headers]{background:#ffd17326;color:var(--ayu-accent-yellow);border:1px solid rgba(255,209,115,.3)}.auth-badge[data-auth-type=oauth]{background:#d4bfff26;color:var(--ayu-accent-purple);border:1px solid rgba(212,191,255,.3)}.server-actions{position:relative}.action-btn{background:transparent;border:none;color:var(--text-tertiary);cursor:pointer;padding:4px;border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;transition:all var(--transition-fast)}.action-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.menu-btn:hover{background:var(--bg-active);color:var(--blue-500)}.dropdown-menu{position:absolute;top:calc(100% + 4px);right:0;background:var(--bg-tertiary);border:1px solid var(--border-bright);border-radius:var(--radius-md);box-shadow:var(--shadow-lg);min-width:180px;z-index:1000;animation:fadeIn .15s ease-out;overflow:hidden}.dropdown-item{width:100%;padding:10px 14px;background:transparent;border:none;color:var(--text-primary);font-size:13px;font-weight:500;text-align:left;cursor:pointer;display:flex;align-items:center;gap:10px;transition:all var(--transition-fast);border-bottom:1px solid transparent}.dropdown-item:hover:not(:disabled){background:var(--bg-hover);color:var(--text-white)}.dropdown-item:disabled{opacity:.5;cursor:not-allowed}.dropdown-item.danger{color:var(--red-500)}.dropdown-item.danger:hover:not(:disabled){background:#f287791a;color:var(--red-500)}.dropdown-item svg{flex-shrink:0;opacity:.7}.dropdown-item:hover svg{opacity:1}.dropdown-divider{height:1px;background:var(--border-color);margin:4px 0}.spinning{animation:spin 1s linear infinite}.tools-sidebar{width:300px;background:var(--bg-secondary);border-right:1px solid var(--border-color);display:flex;flex-direction:column;transform:translateZ(0)}.tools-header{padding:var(--spacing-lg) var(--spacing-xl);border-bottom:1px solid var(--border-color);background:var(--bg-primary);display:flex;align-items:center;justify-content:space-between}.tools-header h3{font-size:14px;font-weight:700;color:var(--text-white);text-transform:uppercase;letter-spacing:1px}.tools-count{background:var(--bg-hover);padding:4px 10px;border-radius:12px;font-size:12px;color:var(--blue-500);font-weight:600;font-family:var(--font-mono)}.tools-search{padding:var(--spacing-md);border-bottom:1px solid var(--border-color);background:var(--bg-primary)}.search-box{width:100%;padding:10px 12px;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:var(--radius-md);color:var(--text-primary);font-size:13px;transition:all var(--transition-fast);font-family:var(--font-sans)}.search-box:focus{border-color:var(--blue-500);background:var(--bg-hover);box-shadow:0 0 0 3px #5ccfe61a;outline:none}.search-box::placeholder{color:var(--text-tertiary)}.tools-list{flex:1;overflow-y:auto;padding:var(--spacing-sm);-webkit-overflow-scrolling:touch}.tool-item{padding:12px var(--spacing-md);margin-bottom:6px;background:var(--bg-tertiary);border-radius:var(--radius-md);cursor:pointer;transition:all var(--transition-fast);border:1px solid transparent}.tool-item:hover{background:var(--bg-hover);border-color:var(--border-bright);transform:translate(2px)}.tool-item.active{background:var(--bg-active);border-color:var(--blue-500);box-shadow:0 0 0 1px var(--blue-500) inset}.tool-name{font-size:13px;font-weight:600;color:var(--text-white);margin-bottom:4px;font-family:var(--font-mono);word-break:break-word}.tool-description{font-size:11px;color:var(--text-secondary);line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.tools-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:var(--spacing-2xl);text-align:center;color:var(--text-tertiary)}.empty-icon{font-size:48px;margin-bottom:var(--spacing-lg);opacity:.6}.tools-empty p{font-size:13px;color:var(--text-secondary);line-height:1.5}.no-results{padding:var(--spacing-2xl) var(--spacing-lg);text-align:center}.no-results p{font-size:13px;color:var(--text-tertiary);line-height:1.5}.copy-btn{display:inline-flex;align-items:center;gap:var(--spacing-xs);padding:6px 12px;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:var(--radius-sm);color:var(--text-secondary);font-size:13px;font-weight:500;cursor:pointer;transition:all var(--transition-fast)}.copy-btn:hover{background:var(--bg-hover);border-color:var(--border-bright);color:var(--text-primary)}.copy-btn:active{transform:scale(.98)}.copy-btn svg{flex-shrink:0}.copy-btn-sm{padding:4px 8px;font-size:12px}.copy-btn:has(svg.lucide-check){color:var(--green-500);border-color:var(--green-500);background:#bae67e1a}.json-viewer{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:var(--radius-md);padding:var(--spacing-lg);font-family:var(--font-mono);font-size:12px;white-space:pre-wrap;word-break:break-word;max-height:400px;overflow-y:auto;line-height:1.6;color:var(--text-primary);-webkit-user-select:text;user-select:text}.json-key{color:var(--ayu-accent-blue);font-weight:500}.json-string{color:var(--ayu-accent-green)}.json-number{color:var(--ayu-accent-orange)}.json-boolean{color:var(--ayu-accent-purple);font-weight:600}.json-null{color:var(--ayu-accent-purple);font-weight:600;font-style:italic}.json-bracket{color:var(--ayu-text-secondary);font-weight:600}.json-punctuation{color:var(--ayu-text-tertiary)}.json-collapse-toggle{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;margin-right:2px;cursor:pointer;color:var(--text-tertiary);transition:color var(--transition-fast);vertical-align:middle;-webkit-user-select:none;user-select:none}.json-collapse-toggle:hover{color:var(--blue-500)}.json-collapse-toggle svg{flex-shrink:0}.json-ellipsis{color:var(--text-tertiary);font-style:italic;margin:0 4px}.json-viewer::selection,.json-viewer *::selection{background:var(--ayu-accent-blue);color:var(--bg-primary)}.json-editor-wrapper{position:relative;width:100%;min-height:200px;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:var(--radius-md);overflow:hidden;transition:border-color var(--transition-fast)}.json-editor-wrapper:focus-within{border-color:var(--blue-500)}.json-editor-highlight{position:absolute;inset:0;padding:var(--spacing-lg);pointer-events:none;overflow:hidden;white-space:pre-wrap;word-wrap:break-word;font-family:var(--font-mono);font-size:13px;line-height:1.5;color:transparent}.json-editor-highlight pre{margin:0;padding:0;font-family:inherit;font-size:inherit;line-height:inherit;white-space:pre-wrap;word-wrap:break-word}.json-editor-input{position:relative;width:100%;min-height:200px;padding:var(--spacing-lg);background:transparent;border:none;color:var(--text-primary);font-family:var(--font-mono);font-size:13px;line-height:1.5;resize:vertical;outline:none;white-space:pre-wrap;word-wrap:break-word;caret-color:var(--text-primary)}.json-editor-input::placeholder{color:var(--text-tertiary)}.json-editor-highlight .json-key{color:var(--ayu-accent-blue);font-weight:500}.json-editor-highlight .json-string{color:var(--ayu-accent-green)}.json-editor-highlight .json-number{color:var(--ayu-accent-orange)}.json-editor-highlight .json-boolean{color:var(--ayu-accent-purple);font-weight:600}.json-editor-highlight .json-null{color:var(--ayu-accent-purple);font-weight:600;font-style:italic}.json-editor-highlight .json-bracket{color:var(--ayu-text-secondary);font-weight:600}.json-editor-highlight .json-punctuation{color:var(--ayu-text-tertiary)}.main-area{flex:1;display:flex;flex-direction:column;overflow:hidden;transform:translateZ(0)}.main-header{padding:var(--spacing-2xl);border-bottom:1px solid var(--border-color);background:var(--bg-secondary)}.main-header h2{font-size:22px;font-weight:700;color:var(--text-white);margin-bottom:var(--spacing-sm);font-family:var(--font-mono);letter-spacing:-.5px}.main-header p{font-size:14px;color:var(--text-secondary);line-height:1.5}.description-container{position:relative}.main-tool-description{font-size:14px;color:var(--text-secondary);line-height:1.5;margin:0;white-space:pre-wrap;word-break:break-word}.main-tool-description.collapsed{display:-webkit-box;-webkit-line-clamp:3;line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis}.description-toggle{margin-top:var(--spacing-xs);padding:0;background:transparent;border:none;color:var(--text-tertiary);cursor:pointer;font-size:12px;font-weight:400;transition:color var(--transition-fast);text-decoration:underline;text-underline-offset:2px}.description-toggle:hover{color:var(--blue-500)}.content-area{flex:1;overflow-y:auto;padding:var(--spacing-2xl);-webkit-overflow-scrolling:touch}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--text-tertiary)}.empty-state-icon{font-size:48px;margin-bottom:var(--spacing-lg)}.empty-state h3{margin-bottom:var(--spacing-sm)}.section{margin-bottom:32px}.section-title{font-size:12px;font-weight:600;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px;margin-bottom:var(--spacing-md)}.schema-viewer{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:var(--radius-md);padding:var(--spacing-lg);font-family:var(--font-mono);font-size:12px;color:var(--text-primary);overflow-x:auto;line-height:1.6}.schema-viewer::selection{background:var(--blue-600);color:var(--bg-primary)}.input-section{margin-top:var(--spacing-2xl)}.mode-toggle{display:flex;gap:var(--spacing-sm);margin-bottom:var(--spacing-md)}.mode-btn{padding:6px 12px;background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-secondary);border-radius:var(--radius-sm);cursor:pointer;font-size:12px;transition:all var(--transition-fast)}.mode-btn.active{background:var(--blue-500);border-color:var(--blue-500);color:#fff}.mode-btn:hover:not(.active){background:var(--bg-hover)}.json-error{margin-top:var(--spacing-md);padding:10px 12px;background:#f287791a;border:1px solid rgba(242,135,121,.3);border-radius:var(--radius-md);color:var(--red-500);font-size:12px;font-family:var(--font-mono);line-height:1.5}.form-fields{display:flex;flex-direction:column;gap:var(--spacing-lg)}.form-field{display:flex;flex-direction:column}.form-label{font-size:13px;font-weight:500;color:var(--text-primary);margin-bottom:6px;display:flex;align-items:center;gap:var(--spacing-xs)}.form-label.checkbox-label{flex-direction:row;cursor:pointer;margin-bottom:0}.form-label.checkbox-label span{display:flex;align-items:center;gap:var(--spacing-xs)}.param-name{border-bottom:1px dashed var(--text-tertiary);cursor:help}.field-type{font-size:11px;color:var(--text-tertiary);font-weight:400;font-family:var(--font-mono)}.form-label .required{color:var(--red-500)}.form-checkbox{width:18px;height:18px;cursor:pointer;accent-color:var(--blue-500)}.form-input{padding:10px 12px;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:var(--radius-md);color:var(--text-primary);font-size:13px;transition:border-color var(--transition-fast)}.form-input:focus{border-color:var(--blue-500)}.form-textarea{padding:10px 12px;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:var(--radius-md);color:var(--text-primary);font-size:13px;font-family:var(--font-mono);resize:vertical;min-height:80px;transition:border-color var(--transition-fast);line-height:1.5}.form-textarea:focus{border-color:var(--blue-500)}.execute-btn{padding:12px 24px;background:linear-gradient(135deg,var(--green-500) 0%,var(--green-600) 100%);color:var(--bg-primary);border:none;border-radius:var(--radius-md);cursor:pointer;font-size:14px;font-weight:700;margin-top:var(--spacing-lg);transition:all var(--transition-fast);box-shadow:0 2px 8px #bae67e33;text-transform:uppercase;letter-spacing:.5px;display:flex;align-items:center;justify-content:center;gap:var(--spacing-md)}.execute-timer{font-family:var(--font-mono);font-size:14px;padding:4px 12px;background:#0003;border-radius:var(--radius-sm);font-weight:600;letter-spacing:.5px;animation:pulse 1s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.8;transform:scale(1.05)}}.execute-btn:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 6px 16px #bae67e4d}.execute-btn:active:not(:disabled){transform:translateY(0)}.execute-btn:disabled{cursor:not-allowed}.execute-btn:disabled:not(.executing){background:var(--bg-hover);opacity:.5;box-shadow:none}.result-section{margin-top:32px}.result-header{display:flex;align-items:center;justify-content:space-between;gap:var(--spacing-md);padding:var(--spacing-md);background:#bae67e1a;border-radius:var(--radius-md);border-left:3px solid var(--green-500);margin-bottom:var(--spacing-md)}.result-header-left{display:flex;align-items:center;gap:var(--spacing-md)}.result-header.error{background:#f287791a;border-left-color:var(--red-500)}.result-status{font-size:14px;font-weight:700;color:var(--green-500);font-family:var(--font-mono)}.result-status.error{color:var(--red-500)}.result-time{font-size:12px;color:var(--text-tertiary);font-family:var(--font-mono)}.result-tabs{display:flex;gap:4px;margin-bottom:var(--spacing-md);border-bottom:1px solid var(--border-color)}.result-tab{padding:8px 16px;background:transparent;border:none;color:var(--text-secondary);cursor:pointer;font-size:13px;border-bottom:2px solid transparent;transition:all var(--transition-fast)}.result-tab.active{color:var(--blue-500);border-bottom-color:var(--blue-500)}.result-tab:hover:not(.active){color:var(--text-primary)}.modal-overlay{position:fixed;inset:0;background:#1f2430e6;display:flex;align-items:center;justify-content:center;z-index:9999;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);animation:fadeIn .2s ease-out}.modal{background:var(--bg-secondary);border-radius:var(--radius-lg);width:90%;max-width:520px;padding:32px;border:1px solid var(--border-bright);box-shadow:var(--shadow-lg);animation:slideUp .25s ease-out;transform:translateZ(0)}.modal-small{max-width:420px;padding:24px}.modal-header h2{font-size:20px;font-weight:700;margin-bottom:24px;color:var(--text-white);letter-spacing:-.3px}.modal-body{margin-bottom:24px}.modal-body .form-field{margin-bottom:20px}.modal-body .form-field:last-child{margin-bottom:0}.modal-body .form-label{display:block;font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:8px;letter-spacing:.2px}.modal-body .form-input{width:100%;padding:10px 12px;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:var(--radius-md);color:var(--text-primary);font-size:14px;font-family:var(--font-sans);transition:all var(--transition-fast)}.modal-body .form-input:focus{border-color:var(--blue-500);background:var(--bg-hover);box-shadow:0 0 0 3px #5ccfe61a;outline:none}.modal-body .form-input::placeholder{color:var(--text-tertiary)}.error-message{background:#f287791a;border:1px solid rgba(242,135,121,.3);color:var(--red-500);padding:12px 14px;border-radius:var(--radius-md);font-size:13px;margin-bottom:20px;line-height:1.5;white-space:pre-line;max-height:300px;overflow-y:auto}.info-message{background:#5ccfe61a;border:1px solid rgba(92,207,230,.3);color:var(--blue-500);padding:10px 12px;border-radius:var(--radius-md);font-size:12px;line-height:1.5}.modal-footer{display:flex;gap:12px;justify-content:flex-end;padding-top:8px}.btn{padding:11px 24px;border:none;border-radius:var(--radius-md);cursor:pointer;font-size:14px;font-weight:600;transition:all var(--transition-fast);letter-spacing:.2px}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-secondary{background:var(--bg-hover);color:var(--text-primary)}.btn-secondary:hover:not(:disabled){background:#404040}.btn-primary{background:linear-gradient(135deg,var(--blue-500) 0%,var(--blue-600) 100%);color:var(--bg-primary);font-weight:700;box-shadow:0 2px 8px #5ccfe633}.btn-primary:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 4px 12px #5ccfe64d}.btn-primary:active:not(:disabled){transform:translateY(0)}.btn-danger{background:linear-gradient(135deg,var(--red-500) 0%,#d96459 100%);color:var(--bg-primary);font-weight:700;box-shadow:0 2px 8px #f2877933}.btn-danger:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 4px 12px #f287794d}.btn-danger:active:not(:disabled){transform:translateY(0)}.radio-group{display:flex;gap:20px;margin-bottom:4px}.radio-option{display:flex;align-items:center;gap:8px;cursor:pointer;padding:4px 0}.radio-option input[type=radio]{accent-color:var(--blue-500);cursor:pointer;width:16px;height:16px}.radio-option span{font-size:14px;color:var(--text-primary);font-weight:500}.oauth-callback{display:flex;align-items:center;justify-content:center;min-height:100vh;background:var(--bg-primary);padding:var(--spacing-2xl)}.oauth-callback-content{max-width:520px;width:100%;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:var(--radius-lg);padding:48px 40px;text-align:center;animation:fadeIn .3s ease-out}.oauth-icon{font-size:72px;margin-bottom:32px;line-height:1}.spinner{animation:pulse 1.5s ease-in-out infinite}.success-icon{color:var(--green-500);font-size:72px;animation:scaleIn .4s cubic-bezier(.175,.885,.32,1.275)}.error-icon{color:var(--red-500);font-size:72px;animation:shake .5s ease-in-out}.oauth-title{font-size:26px;font-weight:700;color:var(--text-white);margin-bottom:16px;letter-spacing:-.5px;line-height:1.3}.oauth-message{font-size:15px;color:var(--text-secondary);margin-bottom:32px;line-height:1.6;max-width:400px;margin-left:auto;margin-right:auto}.oauth-redirect{font-size:13px;color:var(--text-tertiary);font-style:italic;margin-top:24px;opacity:.8}@keyframes scaleIn{0%{transform:scale(0);opacity:0}50%{transform:scale(1.1)}to{transform:scale(1);opacity:1}}@keyframes shake{0%,to{transform:translate(0)}25%{transform:translate(-10px)}75%{transform:translate(10px)}}.toast-container{position:fixed;top:var(--spacing-2xl);right:var(--spacing-2xl);z-index:9999;display:flex;flex-direction:column;gap:var(--spacing-md);pointer-events:none}.toast{min-width:320px;max-width:480px;background:var(--bg-secondary);border:1px solid var(--border-bright);border-radius:var(--radius-lg);padding:var(--spacing-lg);display:flex;align-items:flex-start;gap:var(--spacing-md);box-shadow:var(--shadow-lg);pointer-events:all;animation:slideIn .3s cubic-bezier(.16,1,.3,1)}.toast-icon{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:var(--radius-md)}.toast-success .toast-icon{color:var(--green-500);background:#bae67e26}.toast-error .toast-icon{color:var(--red-500);background:#f2877926}.toast-warning .toast-icon{color:var(--orange-500);background:#ffb27326}.toast-info .toast-icon{color:var(--blue-500);background:#73d0ff26}.toast-content{flex:1;min-width:0}.toast-message{font-size:14px;font-weight:600;color:var(--text-white);margin-bottom:4px;line-height:1.4}.toast-description{font-size:13px;color:var(--text-secondary);line-height:1.5}.toast-close{flex-shrink:0;background:transparent;border:none;color:var(--text-tertiary);cursor:pointer;padding:4px;border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;transition:all var(--transition-fast)}.toast-close:hover{background:var(--bg-hover);color:var(--text-primary)}@keyframes slideIn{0%{transform:translate(100%);opacity:0}to{transform:translate(0);opacity:1}}@media(max-width:768px){.toast-container{top:var(--spacing-lg);right:var(--spacing-lg);left:var(--spacing-lg)}.toast{min-width:unset;width:100%}}.error-boundary{display:flex;align-items:center;justify-content:center;min-height:400px;padding:var(--spacing-2xl);background:var(--bg-primary)}.error-boundary-content{max-width:520px;text-align:center}.error-icon{display:inline-flex;align-items:center;justify-content:center;width:96px;height:96px;border-radius:50%;background:#f2877926;color:var(--red-500);margin-bottom:var(--spacing-xl);animation:shake .5s ease-in-out}.error-title{font-size:24px;font-weight:700;color:var(--text-white);margin-bottom:var(--spacing-md);letter-spacing:-.5px}.error-message{font-size:15px;color:var(--text-secondary);line-height:1.6;margin-bottom:var(--spacing-xl)}.error-details{margin:var(--spacing-xl) 0;text-align:left;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:var(--radius-md);padding:var(--spacing-md)}.error-details summary{font-size:13px;font-weight:600;color:var(--text-primary);cursor:pointer;-webkit-user-select:none;user-select:none;padding:var(--spacing-sm)}.error-details summary:hover{color:var(--text-white)}.error-stack{margin-top:var(--spacing-md);font-family:var(--font-mono);font-size:12px;color:var(--text-tertiary);overflow-x:auto;padding:var(--spacing-md);background:var(--bg-primary);border-radius:var(--radius-sm);line-height:1.6}.error-boundary .btn{display:inline-flex;align-items:center;gap:var(--spacing-sm)}@keyframes shake{0%,to{transform:translate(0)}20%,60%{transform:translate(-8px)}40%,80%{transform:translate(8px)}}.app{display:flex;height:100vh;overflow:hidden}:root{--ayu-bg-primary: #1f2430;--ayu-bg-secondary: #232834;--ayu-bg-tertiary: #2a2f3d;--ayu-bg-hover: #2d3241;--ayu-bg-active: #34405e;--ayu-border: #33415e;--ayu-border-bright: #3e4b59;--ayu-text-primary: #cccac2;--ayu-text-secondary: #707a8c;--ayu-text-tertiary: #5c6773;--ayu-text-white: #d9d7ce;--ayu-accent-cyan: #5ccfe6;--ayu-accent-blue: #73d0ff;--ayu-accent-green: #bae67e;--ayu-accent-orange: #ffae57;--ayu-accent-red: #f28779;--ayu-accent-purple: #d4bfff;--ayu-accent-yellow: #ffd173;--bg-primary: var(--ayu-bg-primary);--bg-secondary: var(--ayu-bg-secondary);--bg-tertiary: var(--ayu-bg-tertiary);--bg-hover: var(--ayu-bg-hover);--bg-active: var(--ayu-bg-active);--text-primary: var(--ayu-text-primary);--text-secondary: var(--ayu-text-secondary);--text-tertiary: var(--ayu-text-tertiary);--text-white: var(--ayu-text-white);--border-color: var(--ayu-border);--border-bright: var(--ayu-border-bright);--blue-500: var(--ayu-accent-cyan);--blue-600: var(--ayu-accent-blue);--green-500: var(--ayu-accent-green);--green-600: #95c74e;--red-500: var(--ayu-accent-red);--yellow-500: var(--ayu-accent-yellow);--purple-500: var(--ayu-accent-purple);--orange-500: var(--ayu-accent-orange);--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-mono: "JetBrains Mono", "Monaco", "Menlo", "Courier New", monospace;--spacing-xs: 4px;--spacing-sm: 8px;--spacing-md: 12px;--spacing-lg: 16px;--spacing-xl: 20px;--spacing-2xl: 24px;--radius-sm: 4px;--radius-md: 6px;--radius-lg: 8px;--shadow-sm: 0 1px 3px rgba(0, 0, 0, .3);--shadow-md: 0 4px 12px rgba(0, 0, 0, .4);--shadow-lg: 0 8px 24px rgba(0, 0, 0, .5);--transition-fast: .15s cubic-bezier(.4, 0, .2, 1);--transition-base: .2s cubic-bezier(.4, 0, .2, 1)}*{margin:0;padding:0;box-sizing:border-box}body{font-family:var(--font-sans);background:var(--bg-primary);color:var(--text-primary);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga" 1,"calt" 1}#root{height:100vh;overflow:hidden;transform:translateZ(0);will-change:auto}button,.clickable{-webkit-user-select:none;user-select:none}code,pre{font-family:var(--font-mono);font-variant-ligatures:common-ligatures}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-track{background:var(--bg-secondary)}::-webkit-scrollbar-thumb{background:var(--ayu-border-bright);border-radius:4px;border:2px solid var(--bg-secondary)}::-webkit-scrollbar-thumb:hover{background:var(--text-tertiary)}*:focus-visible{outline:2px solid var(--blue-500);outline-offset:2px}::selection{background:var(--ayu-accent-blue);color:var(--ayu-bg-primary)}@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}@keyframes slideIn{0%{opacity:0;transform:translate(-8px)}to{opacity:1;transform:translate(0)}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.animate-fade-in{animation:fadeIn .2s ease-out}.animate-slide-in{animation:slideIn .3s ease-out}.animate-spin{animation:spin 1s linear infinite}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.gpu-accelerated{transform:translateZ(0);backface-visibility:hidden;perspective:1000px}.font-mono{font-family:var(--font-mono)}