| @@ -48,6 +48,34 @@ const mcpCode = `<span class="text-ink-muted">$</span> executor mcp --stdio · 47 unmodified lines |
| 48 | </div> | 48 | </div> |
| 49 | </div> | 49 | </div> |
| 50 | | 50 | |
| | 51 | <!-- URL input bar --> |
| | 52 | <div class="rise-d4 mb-4" id="detect-bar"> |
| | 53 | <div class="relative"> |
| | 54 | <div class="flex items-center gap-3 bg-surface-raised border border-rule rounded-xl px-5 py-3.5 focus-within:border-teal/30 transition-colors"> |
| | 55 | <svg class="w-4 h-4 text-ink-faint shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.414-5.874a4.5 4.5 0 00-6.364 0l-4.5 4.5a4.5 4.5 0 001.242 7.244" /></svg> |
| | 56 | <input |
| | 57 | id="detect-input" |
| | 58 | type="url" |
| | 59 | placeholder="Paste any API URL to detect tools..." |
| | 60 | class="flex-1 bg-transparent text-[14px] text-ink placeholder:text-ink-faint outline-none font-mono" |
| | 61 | autocomplete="off" |
| | 62 | spellcheck="false" |
| | 63 | /> |
| | 64 | <button id="detect-btn" class="text-[12px] font-medium text-surface bg-teal hover:bg-teal-dim px-4 py-1.5 rounded-md transition-colors shrink-0 disabled:opacity-40 disabled:cursor-not-allowed"> |
| | 65 | Detect |
| | 66 | </button> |
| | 67 | </div> |
| | 68 | <div id="detect-status" class="hidden absolute left-0 right-0 -bottom-7 text-[11px] font-mono text-center"></div> |
| | 69 | <div id="detect-examples" class="mt-3 flex flex-wrap items-center gap-2 justify-center"> |
| | 70 | <span class="text-[11px] text-ink-faint">Try:</span> |
| | 71 | <button class="detect-example text-[11px] font-mono text-teal/60 hover:text-teal border border-rule hover:border-teal/20 rounded px-2 py-1 transition-colors" data-url="https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json">GitHub REST</button> |
| | 72 | <button class="detect-example text-[11px] font-mono text-teal/60 hover:text-teal border border-rule hover:border-teal/20 rounded px-2 py-1 transition-colors" data-url="https://api.linear.app/graphql">Linear GraphQL</button> |
| | 73 | <button class="detect-example text-[11px] font-mono text-teal/60 hover:text-teal border border-rule hover:border-teal/20 rounded px-2 py-1 transition-colors" data-url="https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest">Google Calendar</button> |
| | 74 | <button class="detect-example text-[11px] font-mono text-teal/60 hover:text-teal border border-rule hover:border-teal/20 rounded px-2 py-1 transition-colors" data-url="https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json">Stripe</button> |
| | 75 | </div> |
| | 76 | </div> |
| | 77 | </div> |
| | 78 | |
| 51 | <!-- Full-width interactive catalog UI --> | 79 | <!-- Full-width interactive catalog UI --> |
| 52 | <div class="rise-d4 relative" id="hero-catalog"> | 80 | <div class="rise-d4 relative" id="hero-catalog"> |
| 53 | <div class="absolute -inset-px rounded-xl bg-gradient-to-b from-teal/10 via-transparent to-transparent pointer-events-none"></div> | 81 | <div class="absolute -inset-px rounded-xl bg-gradient-to-b from-teal/10 via-transparent to-transparent pointer-events-none"></div> |
| @@ -123,19 +151,22 @@ const mcpCode = `<span class="text-ink-muted">$</span> executor mcp --stdio · 69 unmodified lines |
| 123 | </button> | 151 | </button> |
| 124 | </div> | 152 | </div> |
| 125 | | 153 | |
| 126 | <!-- Main: tool list --> | 154 | <!-- Main: tool tree --> |
| 127 | <div class="flex-1 min-w-0"> | 155 | <div class="flex-1 min-w-0 flex flex-col"> |
| 128 | <!-- Search --> | 156 | <!-- Search --> |
| 129 | <div class="px-4 py-3 border-b border-rule"> | 157 | <div class="px-4 py-3 border-b border-rule shrink-0"> |
| 130 | <div class="flex items-center gap-2 px-3 py-2 rounded-md border border-rule bg-surface text-ink-faint text-[12px]"> | 158 | <div class="flex items-center gap-2 px-3 py-2 rounded-md border border-rule bg-surface text-ink-faint text-[12px]"> |
| 131 | <svg class="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" /></svg> | 159 | <svg class="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" /></svg> |
| 132 | <span data-search-label>Search 347 tools...</span> | 160 | <span data-search-label>Search 347 tools...</span> |
| 133 | </div> | 161 | </div> |
| 134 | </div> | 162 | </div> |
| 135 | | 163 | |
| 136 | <!-- Tool rows container --> | 164 | <!-- Scrollable tool tree with bottom fade --> |
| 137 | <div class="divide-y divide-rule" data-tool-list> | 165 | <div class="relative flex-1 min-h-0"> |
| 138 | <!-- Populated by JS --> | 166 | <div class="overflow-y-auto h-[340px] scrollbar-thin" data-tool-list> |
| | 167 | <!-- Populated by JS --> |
| | 168 | </div> |
| | 169 | <div class="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-t from-surface-raised to-transparent pointer-events-none transition-opacity duration-300" data-fade></div> |
| 139 | </div> | 170 | </div> |
| 140 | </div> | 171 | </div> |
| 141 | | 172 | |
| @@ -149,60 +180,116 @@ const mcpCode = `<span class="text-ink-muted">$</span> executor mcp --stdio · 7 unmodified lines |
| 149 | name: 'GitHub', count: 347, tools: [ | 180 | name: 'GitHub', count: 347, tools: [ |
| 150 | { name: 'issues.list', desc: 'List repository issues', policy: 'read', method: 'GET' }, | 181 | { name: 'issues.list', desc: 'List repository issues', policy: 'read', method: 'GET' }, |
| 151 | { name: 'issues.create', desc: 'Create a new issue', policy: 'write', method: 'POST' }, | 182 | { name: 'issues.create', desc: 'Create a new issue', policy: 'write', method: 'POST' }, |
| | 183 | { name: 'issues.get', desc: 'Get a single issue', policy: 'read', method: 'GET' }, |
| | 184 | { name: 'issues.update', desc: 'Update an issue', policy: 'write', method: 'PATCH' }, |
| | 185 | { name: 'issues.lock', desc: 'Lock an issue conversation', policy: 'write', method: 'PUT' }, |
| | 186 | { name: 'issues.comments.list', desc: 'List issue comments', policy: 'read', method: 'GET' }, |
| | 187 | { name: 'issues.comments.create', desc: 'Create an issue comment', policy: 'write', method: 'POST' }, |
| | 188 | { name: 'issues.labels.list', desc: 'List labels on an issue', policy: 'read', method: 'GET' }, |
| | 189 | { name: 'issues.labels.add', desc: 'Add labels to an issue', policy: 'write', method: 'POST' }, |
| 152 | { name: 'pulls.list', desc: 'List pull requests', policy: 'read', method: 'GET' }, | 190 | { name: 'pulls.list', desc: 'List pull requests', policy: 'read', method: 'GET' }, |
| | 191 | { name: 'pulls.get', desc: 'Get a pull request', policy: 'read', method: 'GET' }, |
| | 192 | { name: 'pulls.create', desc: 'Create a pull request', policy: 'write', method: 'POST' }, |
| 153 | { name: 'pulls.merge', desc: 'Merge a pull request', policy: 'destructive', method: 'PUT' }, | 193 | { name: 'pulls.merge', desc: 'Merge a pull request', policy: 'destructive', method: 'PUT' }, |
| | 194 | { name: 'pulls.reviews.list', desc: 'List reviews on a PR', policy: 'read', method: 'GET' }, |
| | 195 | { name: 'pulls.reviews.create', desc: 'Create a PR review', policy: 'write', method: 'POST' }, |
| 154 | { name: 'repos.get', desc: 'Get a repository', policy: 'read', method: 'GET' }, | 196 | { name: 'repos.get', desc: 'Get a repository', policy: 'read', method: 'GET' }, |
| | 197 | { name: 'repos.list', desc: 'List user repositories', policy: 'read', method: 'GET' }, |
| | 198 | { name: 'repos.create', desc: 'Create a repository', policy: 'write', method: 'POST' }, |
| | 199 | { name: 'repos.delete', desc: 'Delete a repository', policy: 'destructive', method: 'DELETE' }, |
| 155 | { name: 'actions.workflows.list', desc: 'List repository workflows', policy: 'read', method: 'GET' }, | 200 | { name: 'actions.workflows.list', desc: 'List repository workflows', policy: 'read', method: 'GET' }, |
| | 201 | { name: 'actions.workflows.dispatch', desc: 'Trigger a workflow run', policy: 'write', method: 'POST' }, |
| | 202 | { name: 'actions.runs.list', desc: 'List workflow runs', policy: 'read', method: 'GET' }, |
| | 203 | { name: 'actions.runs.cancel', desc: 'Cancel a workflow run', policy: 'destructive', method: 'POST' }, |
| 156 | ] | 204 | ] |
| 157 | }, | 205 | }, |
| 158 | slack: { | 206 | slack: { |
| 159 | name: 'Slack', count: 42, tools: [ | 207 | name: 'Slack', count: 42, tools: [ |
| 160 | { name: 'chat.postMessage', desc: 'Send a message to a channel', policy: 'write', method: 'POST' }, | 208 | { name: 'chat.postMessage', desc: 'Send a message to a channel', policy: 'write', method: 'POST' }, |
| | 209 | { name: 'chat.update', desc: 'Update an existing message', policy: 'write', method: 'POST' }, |
| | 210 | { name: 'chat.delete', desc: 'Delete a message', policy: 'destructive', method: 'POST' }, |
| 161 | { name: 'conversations.list', desc: 'List all channels', policy: 'read', method: 'GET' }, | 211 | { name: 'conversations.list', desc: 'List all channels', policy: 'read', method: 'GET' }, |
| 162 | { name: 'conversations.history', desc: 'Fetch message history', policy: 'read', method: 'GET' }, | 212 | { name: 'conversations.history', desc: 'Fetch message history', policy: 'read', method: 'GET' }, |
| | 213 | { name: 'conversations.create', desc: 'Create a new channel', policy: 'write', method: 'POST' }, |
| | 214 | { name: 'conversations.archive', desc: 'Archive a channel', policy: 'destructive', method: 'POST' }, |
| | 215 | { name: 'conversations.members', desc: 'List channel members', policy: 'read', method: 'GET' }, |
| 163 | { name: 'users.list', desc: 'List all users in workspace', policy: 'read', method: 'GET' }, | 216 | { name: 'users.list', desc: 'List all users in workspace', policy: 'read', method: 'GET' }, |
| | 217 | { name: 'users.info', desc: 'Get user profile info', policy: 'read', method: 'GET' }, |
| 164 | { name: 'reactions.add', desc: 'Add a reaction to a message', policy: 'write', method: 'POST' }, | 218 | { name: 'reactions.add', desc: 'Add a reaction to a message', policy: 'write', method: 'POST' }, |
| | 219 | { name: 'reactions.remove', desc: 'Remove a reaction', policy: 'write', method: 'POST' }, |
| 165 | { name: 'files.upload', desc: 'Upload a file to a channel', policy: 'write', method: 'POST' }, | 220 | { name: 'files.upload', desc: 'Upload a file to a channel', policy: 'write', method: 'POST' }, |
| | 221 | { name: 'files.list', desc: 'List files in workspace', policy: 'read', method: 'GET' }, |
| 166 | ] | 222 | ] |
| 167 | }, | 223 | }, |
| 168 | calendar: { | 224 | calendar: { |
| 169 | name: 'Google Calendar', count: 18, tools: [ | 225 | name: 'Google Calendar', count: 18, tools: [ |
| 170 | { name: 'events.list', desc: 'List upcoming events', policy: 'read', method: 'GET' }, | 226 | { name: 'events.list', desc: 'List upcoming events', policy: 'read', method: 'GET' }, |
| | 227 | { name: 'events.get', desc: 'Get a single event', policy: 'read', method: 'GET' }, |
| 171 | { name: 'events.insert', desc: 'Create a new event', policy: 'write', method: 'POST' }, | 228 | { name: 'events.insert', desc: 'Create a new event', policy: 'write', method: 'POST' }, |
| | 229 | { name: 'events.update', desc: 'Update an existing event', policy: 'write', method: 'PATCH' }, |
| 172 | { name: 'events.delete', desc: 'Delete an event', policy: 'destructive', method: 'DELETE' }, | 230 | { name: 'events.delete', desc: 'Delete an event', policy: 'destructive', method: 'DELETE' }, |
| | 231 | { name: 'events.move', desc: 'Move event to another calendar', policy: 'write', method: 'POST' }, |
| 173 | { name: 'calendars.get', desc: 'Get calendar metadata', policy: 'read', method: 'GET' }, | 232 | { name: 'calendars.get', desc: 'Get calendar metadata', policy: 'read', method: 'GET' }, |
| 174 | { name: 'events.update', desc: 'Update an existing event', policy: 'write', method: 'PATCH' }, | 233 | { name: 'calendars.insert', desc: 'Create a secondary calendar', policy: 'write', method: 'POST' }, |
| | 234 | { name: 'calendars.delete', desc: 'Delete a calendar', policy: 'destructive', method: 'DELETE' }, |
| | 235 | { name: 'calendarList.list', desc: 'List calendars on user list', policy: 'read', method: 'GET' }, |
| 175 | { name: 'freebusy.query', desc: 'Check availability', policy: 'read', method: 'POST' }, | 236 | { name: 'freebusy.query', desc: 'Check availability', policy: 'read', method: 'POST' }, |
| | 237 | { name: 'acl.list', desc: 'List access control rules', policy: 'read', method: 'GET' }, |
| | 238 | { name: 'acl.insert', desc: 'Add an access control rule', policy: 'write', method: 'POST' }, |
| 176 | ] | 239 | ] |
| 177 | }, | 240 | }, |
| 178 | linear: { | 241 | linear: { |
| 179 | name: 'Linear', count: 56, tools: [ | 242 | name: 'Linear', count: 56, tools: [ |
| 180 | { name: 'issues.list', desc: 'List issues with filters', policy: 'read', method: 'query' }, | 243 | { name: 'issues.list', desc: 'List issues with filters', policy: 'read', method: 'query' }, |
| | 244 | { name: 'issues.get', desc: 'Get a single issue', policy: 'read', method: 'query' }, |
| 181 | { name: 'issues.create', desc: 'Create a new issue', policy: 'write', method: 'mutation' }, | 245 | { name: 'issues.create', desc: 'Create a new issue', policy: 'write', method: 'mutation' }, |
| 182 | { name: 'issues.update', desc: 'Update issue status or fields', policy: 'write', method: 'mutation' }, | 246 | { name: 'issues.update', desc: 'Update issue status or fields', policy: 'write', method: 'mutation' }, |
| | 247 | { name: 'issues.delete', desc: 'Delete an issue permanently', policy: 'destructive', method: 'mutation' }, |
| | 248 | { name: 'issues.archive', desc: 'Archive an issue', policy: 'write', method: 'mutation' }, |
| 183 | { name: 'projects.list', desc: 'List all projects', policy: 'read', method: 'query' }, | 249 | { name: 'projects.list', desc: 'List all projects', policy: 'read', method: 'query' }, |
| | 250 | { name: 'projects.create', desc: 'Create a new project', policy: 'write', method: 'mutation' }, |
| | 251 | { name: 'projects.update', desc: 'Update project details', policy: 'write', method: 'mutation' }, |
| | 252 | { name: 'comments.list', desc: 'List comments on an issue', policy: 'read', method: 'query' }, |
| 184 | { name: 'comments.create', desc: 'Add a comment to an issue', policy: 'write', method: 'mutation' }, | 253 | { name: 'comments.create', desc: 'Add a comment to an issue', policy: 'write', method: 'mutation' }, |
| 185 | { name: 'issues.delete', desc: 'Delete an issue permanently', policy: 'destructive', method: 'mutation' }, | 254 | { name: 'cycles.list', desc: 'List all cycles', policy: 'read', method: 'query' }, |
| | 255 | { name: 'teams.list', desc: 'List all teams', policy: 'read', method: 'query' }, |
| | 256 | { name: 'labels.list', desc: 'List all labels', policy: 'read', method: 'query' }, |
| 186 | ] | 257 | ] |
| 187 | }, | 258 | }, |
| 188 | resend: { | 259 | resend: { |
| 189 | name: 'Resend', count: 19, tools: [ | 260 | name: 'Resend', count: 19, tools: [ |
| 190 | { name: 'emails.send', desc: 'Send a transactional email', policy: 'write', method: 'POST' }, | 261 | { name: 'emails.send', desc: 'Send a transactional email', policy: 'write', method: 'POST' }, |
| 191 | { name: 'emails.get', desc: 'Get email delivery status', policy: 'read', method: 'GET' }, | 262 | { name: 'emails.get', desc: 'Get email delivery status', policy: 'read', method: 'GET' }, |
| | 263 | { name: 'emails.batch', desc: 'Send a batch of emails', policy: 'write', method: 'POST' }, |
| 192 | { name: 'domains.list', desc: 'List verified domains', policy: 'read', method: 'GET' }, | 264 | { name: 'domains.list', desc: 'List verified domains', policy: 'read', method: 'GET' }, |
| | 265 | { name: 'domains.get', desc: 'Get domain details', policy: 'read', method: 'GET' }, |
| | 266 | { name: 'domains.create', desc: 'Add a new domain', policy: 'write', method: 'POST' }, |
| 193 | { name: 'domains.verify', desc: 'Verify a domain', policy: 'write', method: 'POST' }, | 267 | { name: 'domains.verify', desc: 'Verify a domain', policy: 'write', method: 'POST' }, |
| | 268 | { name: 'domains.delete', desc: 'Remove a domain', policy: 'destructive', method: 'DELETE' }, |
| 194 | { name: 'apiKeys.list', desc: 'List API keys', policy: 'read', method: 'GET' }, | 269 | { name: 'apiKeys.list', desc: 'List API keys', policy: 'read', method: 'GET' }, |
| | 270 | { name: 'apiKeys.create', desc: 'Create an API key', policy: 'write', method: 'POST' }, |
| | 271 | { name: 'contacts.list', desc: 'List contacts in audience', policy: 'read', method: 'GET' }, |
| 195 | { name: 'contacts.create', desc: 'Create a new contact', policy: 'write', method: 'POST' }, | 272 | { name: 'contacts.create', desc: 'Create a new contact', policy: 'write', method: 'POST' }, |
| | 273 | { name: 'contacts.remove', desc: 'Remove a contact', policy: 'destructive', method: 'DELETE' }, |
| 196 | ] | 274 | ] |
| 197 | }, | 275 | }, |
| 198 | axiom: { | 276 | axiom: { |
| 199 | name: 'Axiom', count: 17, tools: [ | 277 | name: 'Axiom', count: 17, tools: [ |
| 200 | { name: 'queryDataset', desc: 'Query datasets using APL', policy: 'read', method: 'tool' }, | 278 | { name: 'datasets.query', desc: 'Query datasets using APL', policy: 'read', method: 'tool' }, |
| 201 | { name: 'listDatasets', desc: 'List all available datasets', policy: 'read', method: 'tool' }, | 279 | { name: 'datasets.list', desc: 'List all available datasets', policy: 'read', method: 'tool' }, |
| 202 | { name: 'getDatasetFields', desc: 'List all fields in a dataset', policy: 'read', method: 'tool' }, | 280 | { name: 'datasets.fields', desc: 'List all fields in a dataset', policy: 'read', method: 'tool' }, |
| 203 | { name: 'checkMonitors', desc: 'Check all monitors and their statuses', policy: 'read', method: 'tool' }, | 281 | { name: 'datasets.info', desc: 'Get dataset metadata and stats', policy: 'read', method: 'tool' }, |
| 204 | { name: 'listDashboards', desc: 'List all available dashboards', policy: 'read', method: 'tool' }, | 282 | { name: 'datasets.create', desc: 'Create a new dataset', policy: 'write', method: 'tool' }, |
| 205 | { name: 'deleteDashboard', desc: 'Delete a dashboard by UID', policy: 'destructive', method: 'tool' }, | 283 | { name: 'datasets.trim', desc: 'Trim data from a dataset', policy: 'destructive', method: 'tool' }, |
| | 284 | { name: 'monitors.list', desc: 'List all monitors', policy: 'read', method: 'tool' }, |
| | 285 | { name: 'monitors.check', desc: 'Check monitor statuses', policy: 'read', method: 'tool' }, |
| | 286 | { name: 'monitors.create', desc: 'Create a new monitor', policy: 'write', method: 'tool' }, |
| | 287 | { name: 'dashboards.list', desc: 'List all dashboards', policy: 'read', method: 'tool' }, |
| | 288 | { name: 'dashboards.get', desc: 'Get dashboard by ID', policy: 'read', method: 'tool' }, |
| | 289 | { name: 'dashboards.create', desc: 'Create a new dashboard', policy: 'write', method: 'tool' }, |
| | 290 | { name: 'dashboards.delete', desc: 'Delete a dashboard by UID', policy: 'destructive', method: 'tool' }, |
| | 291 | { name: 'annotations.list', desc: 'List all annotations', policy: 'read', method: 'tool' }, |
| | 292 | { name: 'annotations.create', desc: 'Create an annotation', policy: 'write', method: 'tool' }, |
| 206 | ] | 293 | ] |
| 207 | }, | 294 | }, |
| 208 | }; | 295 | }; |
| @@ -229,11 +316,17 @@ const mcpCode = `<span class="text-ink-muted">$</span> executor mcp --stdio · 20 unmodified lines |
| 229 | | 316 | |
| 230 | const catalog = document.getElementById('hero-catalog'); | 317 | const catalog = document.getElementById('hero-catalog'); |
| 231 | const toolList = catalog.querySelector('[data-tool-list]'); | 318 | const toolList = catalog.querySelector('[data-tool-list]'); |
| | 319 | const fadeEl = catalog.querySelector('[data-fade]'); |
| 232 | const breadcrumb = catalog.querySelector('[data-breadcrumb]'); | 320 | const breadcrumb = catalog.querySelector('[data-breadcrumb]'); |
| 233 | const totalTools = catalog.querySelector('[data-total-tools]'); | 321 | const totalTools = catalog.querySelector('[data-total-tools]'); |
| 234 | const searchLabel = catalog.querySelector('[data-search-label]'); | 322 | const searchLabel = catalog.querySelector('[data-search-label]'); |
| 235 | const buttons = catalog.querySelectorAll('.catalog-source'); | | |
| 236 | | 323 | |
| | 324 | function updateFade() { |
| | 325 | const atBottom = toolList.scrollHeight - toolList.scrollTop - toolList.clientHeight < 8; |
| | 326 | const noOverflow = toolList.scrollHeight <= toolList.clientHeight; |
| | 327 | fadeEl.style.opacity = (atBottom || noOverflow) ? '0' : '1'; |
| | 328 | } |
| | 329 | toolList.addEventListener('scroll', updateFade); |
| 237 | function renderSource(key) { | 330 | function renderSource(key) { |
| 238 | const src = sources[key]; | 331 | const src = sources[key]; |
| 239 | breadcrumb.textContent = src.name; | 332 | breadcrumb.textContent = src.name; |
| @@ -242,7 +335,7 @@ const mcpCode = `<span class="text-ink-muted">$</span> executor mcp --stdio · 2 unmodified lines |
| 242 | const totalCount = Object.values(sources).reduce((s, v) => s + v.count, 0); | 335 | const totalCount = Object.values(sources).reduce((s, v) => s + v.count, 0); |
| 243 | totalTools.textContent = `${totalCount} tools indexed`; | 336 | totalTools.textContent = `${totalCount} tools indexed`; |
| 244 | | 337 | |
| 245 | buttons.forEach(btn => { | 338 | catalog.querySelectorAll('.catalog-source').forEach(btn => { |
| 246 | const isActive = btn.dataset.source === key; | 339 | const isActive = btn.dataset.source === key; |
| 247 | if (isActive) { | 340 | if (isActive) { |
| 248 | btn.className = btn.className | 341 | btn.className = btn.className |
| @@ -257,26 +350,173 @@ const mcpCode = `<span class="text-ink-muted">$</span> executor mcp --stdio · 8 unmodified lines |
| 257 | } | 350 | } |
| 258 | }); | 351 | }); |
| 259 | | 352 | |
| 260 | toolList.innerHTML = src.tools.map((tool, i) => { | 353 | // Build tree from dot-separated tool names |
| 261 | const isLast = i === src.tools.length - 1; | 354 | const tree = {}; |
| 262 | const colors = policyColors[tool.policy]; | 355 | for (const tool of src.tools) { |
| 263 | const mColor = methodColors[tool.method] || 'text-ink-faint'; | 356 | const parts = tool.name.split('.'); |
| 264 | return `<div class="flex items-center gap-4 px-4 py-3${isLast ? ' opacity-40' : ''}"> | 357 | let node = tree; |
| 265 | <div class="flex-1 min-w-0"> | 358 | for (let i = 0; i < parts.length - 1; i++) { |
| 266 | <div class="font-mono text-[12px] text-ink">${tool.name}</div> | 359 | if (!node[parts[i]]) node[parts[i]] = { _children: {} }; |
| 267 | <div class="text-[11px] text-ink-faint">${tool.desc}</div> | 360 | node = node[parts[i]]._children; |
| 268 | </div> | 361 | } |
| 269 | <span class="text-[10px] font-mono ${colors} rounded px-1.5 py-0.5 shrink-0">${tool.policy}</span> | 362 | const leaf = parts[parts.length - 1]; |
| 270 | <span class="text-[10px] font-mono ${mColor} shrink-0">${tool.method}</span> | 363 | node[leaf] = { _tool: tool }; |
| 271 | </div>`; | 364 | } |
| 272 | }).join(''); | 365 | |
| | 366 | function renderTree(node, depth = 0) { |
| | 367 | let html = ''; |
| | 368 | const keys = Object.keys(node); |
| | 369 | keys.forEach((key, idx) => { |
| | 370 | const entry = node[key]; |
| | 371 | const isLast = idx === keys.length - 1; |
| | 372 | const indent = depth * 20; |
| | 373 | const connector = isLast ? '└' : '├'; |
| | 374 | |
| | 375 | if (entry._tool) { |
| | 376 | // Leaf: actual tool |
| | 377 | const tool = entry._tool; |
| | 378 | const colors = policyColors[tool.policy]; |
| | 379 | const mColor = methodColors[tool.method] || 'text-ink-faint'; |
| | 380 | html += `<div class="flex items-center gap-3 py-1.5 pr-4 hover:bg-surface-overlay/50 transition-colors" style="padding-left:${16 + indent}px"> |
| | 381 | <span class="text-ink-faint font-mono text-[11px] w-3 shrink-0 select-none">${depth > 0 ? connector : '·'}</span> |
| | 382 | <span class="font-mono text-[12px] text-ink flex-1 min-w-0 truncate">${key}</span> |
| | 383 | <span class="text-[10px] font-mono ${colors} rounded px-1.5 py-0.5 shrink-0">${tool.policy}</span> |
| | 384 | <span class="text-[10px] font-mono ${mColor} shrink-0 w-14 text-right">${tool.method}</span> |
| | 385 | </div>`; |
| | 386 | } else { |
| | 387 | // Branch: namespace group |
| | 388 | const childCount = Object.keys(entry._children).length; |
| | 389 | html += `<div class="flex items-center gap-3 py-1.5 pr-4" style="padding-left:${16 + indent}px"> |
| | 390 | <span class="text-ink-faint font-mono text-[11px] w-3 shrink-0 select-none">${depth > 0 ? connector : '▸'}</span> |
| | 391 | <span class="font-mono text-[12px] text-ink-muted">${key}</span> |
| | 392 | <span class="text-[10px] text-ink-faint">${childCount}</span> |
| | 393 | </div>`; |
| | 394 | html += renderTree(entry._children, depth + 1); |
| | 395 | } |
| | 396 | }); |
| | 397 | return html; |
| | 398 | } |
| | 399 | |
| | 400 | toolList.innerHTML = renderTree(tree); |
| | 401 | toolList.scrollTop = 0; |
| | 402 | updateFade(); |
| 273 | } | 403 | } |
| 274 | | 404 | |
| 275 | buttons.forEach(btn => { | 405 | catalog.querySelectorAll('.catalog-source').forEach(btn => { |
| 276 | btn.addEventListener('click', () => renderSource(btn.dataset.source)); | 406 | btn.addEventListener('click', () => renderSource(btn.dataset.source)); |
| 277 | }); | 407 | }); |
| 278 | | 408 | |
| 279 | renderSource('github'); | 409 | renderSource('github'); |
| | 410 | |
| | 411 | // ── Live detection ── |
| | 412 | const detectInput = document.getElementById('detect-input'); |
| | 413 | const detectBtn = document.getElementById('detect-btn'); |
| | 414 | const detectStatus = document.getElementById('detect-status'); |
| | 415 | const detectExamples = document.getElementById('detect-examples'); |
| | 416 | |
| | 417 | const kindColors = { |
| | 418 | openapi: { bg: 'bg-emerald-400/15', text: 'text-emerald-400', letter: 'O' }, |
| | 419 | graphql: { bg: 'bg-pink-400/15', text: 'text-pink-400', letter: 'Q' }, |
| | 420 | mcp: { bg: 'bg-cyan-400/15', text: 'text-cyan-400', letter: 'M' }, |
| | 421 | googleDiscovery: { bg: 'bg-yellow-400/15', text: 'text-yellow-400', letter: 'G' }, |
| | 422 | }; |
| | 423 | |
| | 424 | // Tailwind safelist for dynamic classes: |
| | 425 | // bg-emerald-400/15 text-emerald-400 bg-pink-400/15 text-pink-400 bg-yellow-400/15 text-yellow-400 |
| | 426 | |
| | 427 | const kindLabels = { openapi: 'openapi', graphql: 'graphql', mcp: 'mcp', googleDiscovery: 'google' }; |
| | 428 | |
| | 429 | let detectedCount = 0; |
| | 430 | |
| | 431 | async function detectUrl(url) { |
| | 432 | detectBtn.disabled = true; |
| | 433 | detectBtn.textContent = 'Detecting...'; |
| | 434 | detectStatus.className = 'absolute left-0 right-0 -bottom-7 text-[11px] font-mono text-center text-ink-muted'; |
| | 435 | detectStatus.textContent = 'Probing URL...'; |
| | 436 | |
| | 437 | try { |
| | 438 | const res = await fetch('/api/detect', { |
| | 439 | method: 'POST', |
| | 440 | headers: { 'Content-Type': 'application/json' }, |
| | 441 | body: JSON.stringify({ url }), |
| | 442 | }); |
| | 443 | const data = await res.json(); |
| | 444 | |
| | 445 | if (!res.ok) { |
| | 446 | detectStatus.className = 'absolute left-0 right-0 -bottom-7 text-[11px] font-mono text-center text-red-400/70'; |
| | 447 | detectStatus.textContent = data.error || 'Detection failed'; |
| | 448 | return; |
| | 449 | } |
| | 450 | |
| | 451 | // Add detected source to the catalog |
| | 452 | detectedCount++; |
| | 453 | const key = `detected_${detectedCount}`; |
| | 454 | const colors = kindColors[data.kind] || kindColors.openapi; |
| | 455 | const kindLabel = kindLabels[data.kind] || data.kind; |
| | 456 | |
| | 457 | sources[key] = { |
| | 458 | name: data.name, |
| | 459 | count: data.count, |
| | 460 | tools: data.tools.map(t => ({ |
| | 461 | name: t.name, |
| | 462 | desc: t.desc, |
| | 463 | policy: t.policy, |
| | 464 | method: t.method, |
| | 465 | })), |
| | 466 | }; |
| | 467 | |
| | 468 | // Add sidebar button |
| | 469 | const sidebar = catalog.querySelector('.space-y-1'); |
| | 470 | const btn = document.createElement('button'); |
| | 471 | btn.dataset.source = key; |
| | 472 | btn.className = 'catalog-source w-full flex items-center gap-2.5 px-2 py-2 rounded-md text-left transition-colors border border-transparent hover:bg-surface-overlay'; |
| | 473 | btn.innerHTML = ` |
| | 474 | <div class="w-5 h-5 rounded ${colors.bg} flex items-center justify-center text-[9px] ${colors.text} font-mono font-medium shrink-0">${data.name.charAt(0).toUpperCase()}</div> |
| | 475 | <div class="flex-1 min-w-0"> |
| | 476 | <div class="text-ink-muted text-[12px] truncate">${data.name}</div> |
| | 477 | <div class="text-[10px] text-ink-faint">${data.count} tools</div> |
| | 478 | </div> |
| | 479 | <span class="text-[9px] font-mono text-ink-faint shrink-0">${kindLabel}</span> |
| | 480 | `; |
| | 481 | btn.addEventListener('click', () => renderSource(key)); |
| | 482 | sidebar.appendChild(btn); |
| | 483 | |
| | 484 | renderSource(key); |
| | 485 | |
| | 486 | detectStatus.className = 'absolute left-0 right-0 -bottom-7 text-[11px] font-mono text-center text-teal'; |
| | 487 | detectStatus.textContent = `${data.kind} detected — ${data.count} tools found`; |
| | 488 | detectExamples.classList.add('hidden'); |
| | 489 | |
| | 490 | // Clear input |
| | 491 | detectInput.value = ''; |
| | 492 | } catch (err) { |
| | 493 | detectStatus.className = 'absolute left-0 right-0 -bottom-7 text-[11px] font-mono text-center text-red-400/70'; |
| | 494 | detectStatus.textContent = 'Network error — could not reach detection API'; |
| | 495 | } finally { |
| | 496 | detectBtn.disabled = false; |
| | 497 | detectBtn.textContent = 'Detect'; |
| | 498 | } |
| | 499 | } |
| | 500 | |
| | 501 | detectBtn.addEventListener('click', () => { |
| | 502 | const url = detectInput.value.trim(); |
| | 503 | if (url) detectUrl(url); |
| | 504 | }); |
| | 505 | |
| | 506 | detectInput.addEventListener('keydown', (e) => { |
| | 507 | if (e.key === 'Enter') { |
| | 508 | const url = detectInput.value.trim(); |
| | 509 | if (url) detectUrl(url); |
| | 510 | } |
| | 511 | }); |
| | 512 | |
| | 513 | // Example buttons |
| | 514 | document.querySelectorAll('.detect-example').forEach(btn => { |
| | 515 | btn.addEventListener('click', () => { |
| | 516 | detectInput.value = btn.dataset.url; |
| | 517 | detectUrl(btn.dataset.url); |
| | 518 | }); |
| | 519 | }); |
| 280 | </script> | 520 | </script> |
| 281 | </div> | 521 | </div> |
| 282 | | 522 | |