screenpipe-mcp 0.4.1 → 0.5.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/dist/index.js CHANGED
@@ -71,7 +71,7 @@ const SCREENPIPE_API = `http://localhost:${port}`;
71
71
  // Initialize server
72
72
  const server = new index_js_1.Server({
73
73
  name: "screenpipe",
74
- version: "0.4.0",
74
+ version: "0.5.0",
75
75
  }, {
76
76
  capabilities: {
77
77
  tools: {},
@@ -469,6 +469,12 @@ const RESOURCES = [
469
469
  description: "How to use screenpipe search effectively",
470
470
  mimeType: "text/markdown",
471
471
  },
472
+ {
473
+ uri: "ui://search",
474
+ name: "Search Dashboard",
475
+ description: "Interactive search UI for exploring screen recordings and audio transcriptions",
476
+ mimeType: "text/html",
477
+ },
472
478
  ];
473
479
  // List resources handler
474
480
  server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
@@ -541,6 +547,56 @@ server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) =
541
547
  },
542
548
  ],
543
549
  };
550
+ case "ui://search": {
551
+ // MCP App UI - Interactive search dashboard
552
+ const uiHtmlPath = path.join(__dirname, "..", "ui", "search.html");
553
+ let htmlContent;
554
+ try {
555
+ htmlContent = fs.readFileSync(uiHtmlPath, "utf-8");
556
+ }
557
+ catch {
558
+ // Fallback: serve embedded minimal UI if file not found
559
+ htmlContent = `<!DOCTYPE html>
560
+ <html>
561
+ <head>
562
+ <style>
563
+ body { font-family: system-ui; background: #0a0a0a; color: #fff; padding: 20px; }
564
+ input { width: 100%; padding: 10px; margin-bottom: 10px; background: #1a1a1a; border: 1px solid #333; color: #fff; border-radius: 6px; }
565
+ button { padding: 10px 20px; background: #fff; color: #000; border: none; border-radius: 6px; cursor: pointer; }
566
+ #results { margin-top: 20px; }
567
+ .result { background: #1a1a1a; padding: 12px; margin: 8px 0; border-radius: 8px; border: 1px solid #333; }
568
+ </style>
569
+ </head>
570
+ <body>
571
+ <h2>screenpipe search</h2>
572
+ <input id="q" placeholder="search..." onkeydown="if(event.key==='Enter')search()"/>
573
+ <button onclick="search()">search</button>
574
+ <div id="results"></div>
575
+ <script>
576
+ function search() {
577
+ window.parent.postMessage({jsonrpc:'2.0',method:'tools/call',params:{name:'search-content',arguments:{q:document.getElementById('q').value,limit:20}}},'*');
578
+ }
579
+ window.addEventListener('message',e=>{
580
+ if(e.data?.result||e.data?.method==='tool/result'){
581
+ const r=e.data.result||e.data.params?.result;
582
+ const d=r?.data||r||[];
583
+ document.getElementById('results').innerHTML=d.map(x=>'<div class="result"><b>'+((x.type||'')+'</b> '+(x.content?.app_name||'')+': '+(x.content?.text||x.content?.transcription||'').substring(0,200))+'</div>').join('');
584
+ }
585
+ });
586
+ </script>
587
+ </body>
588
+ </html>`;
589
+ }
590
+ return {
591
+ contents: [
592
+ {
593
+ uri,
594
+ mimeType: "text/html",
595
+ text: htmlContent,
596
+ },
597
+ ],
598
+ };
599
+ }
544
600
  default:
545
601
  throw new Error(`Unknown resource: ${uri}`);
546
602
  }
package/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "manifest_version": "0.3",
3
3
  "name": "screenpipe",
4
4
  "display_name": "Screenpipe",
5
- "version": "0.3.1",
5
+ "version": "0.5.0",
6
6
  "description": "Search your screen recordings, audio transcriptions, and control your computer with AI",
7
7
  "long_description": "Screenpipe is a 24/7 screen and audio recorder that lets you search everything you've seen or heard. This extension connects Claude to your local screenpipe instance, enabling AI-powered search through your digital memory and computer control capabilities.",
8
8
  "author": {
@@ -30,6 +30,10 @@
30
30
  "name": "search-content",
31
31
  "description": "Search through recorded screen content, audio transcriptions, and UI elements"
32
32
  },
33
+ {
34
+ "name": "export-video",
35
+ "description": "Export screen recordings as MP4 video for a specific time range"
36
+ },
33
37
  {
34
38
  "name": "pixel-control",
35
39
  "description": "Control mouse and keyboard (type text, press keys, move mouse, click)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screenpipe-mcp",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server for screenpipe - search your screen recordings, audio transcriptions, and control your computer",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -51,7 +51,7 @@ const SCREENPIPE_API = `http://localhost:${port}`;
51
51
  const server = new Server(
52
52
  {
53
53
  name: "screenpipe",
54
- version: "0.4.0",
54
+ version: "0.5.0",
55
55
  },
56
56
  {
57
57
  capabilities: {
@@ -466,6 +466,12 @@ const RESOURCES = [
466
466
  description: "How to use screenpipe search effectively",
467
467
  mimeType: "text/markdown",
468
468
  },
469
+ {
470
+ uri: "ui://search",
471
+ name: "Search Dashboard",
472
+ description: "Interactive search UI for exploring screen recordings and audio transcriptions",
473
+ mimeType: "text/html",
474
+ },
469
475
  ];
470
476
 
471
477
  // List resources handler
@@ -543,6 +549,56 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
543
549
  ],
544
550
  };
545
551
 
552
+ case "ui://search": {
553
+ // MCP App UI - Interactive search dashboard
554
+ const uiHtmlPath = path.join(__dirname, "..", "ui", "search.html");
555
+ let htmlContent: string;
556
+ try {
557
+ htmlContent = fs.readFileSync(uiHtmlPath, "utf-8");
558
+ } catch {
559
+ // Fallback: serve embedded minimal UI if file not found
560
+ htmlContent = `<!DOCTYPE html>
561
+ <html>
562
+ <head>
563
+ <style>
564
+ body { font-family: system-ui; background: #0a0a0a; color: #fff; padding: 20px; }
565
+ input { width: 100%; padding: 10px; margin-bottom: 10px; background: #1a1a1a; border: 1px solid #333; color: #fff; border-radius: 6px; }
566
+ button { padding: 10px 20px; background: #fff; color: #000; border: none; border-radius: 6px; cursor: pointer; }
567
+ #results { margin-top: 20px; }
568
+ .result { background: #1a1a1a; padding: 12px; margin: 8px 0; border-radius: 8px; border: 1px solid #333; }
569
+ </style>
570
+ </head>
571
+ <body>
572
+ <h2>screenpipe search</h2>
573
+ <input id="q" placeholder="search..." onkeydown="if(event.key==='Enter')search()"/>
574
+ <button onclick="search()">search</button>
575
+ <div id="results"></div>
576
+ <script>
577
+ function search() {
578
+ window.parent.postMessage({jsonrpc:'2.0',method:'tools/call',params:{name:'search-content',arguments:{q:document.getElementById('q').value,limit:20}}},'*');
579
+ }
580
+ window.addEventListener('message',e=>{
581
+ if(e.data?.result||e.data?.method==='tool/result'){
582
+ const r=e.data.result||e.data.params?.result;
583
+ const d=r?.data||r||[];
584
+ document.getElementById('results').innerHTML=d.map(x=>'<div class="result"><b>'+((x.type||'')+'</b> '+(x.content?.app_name||'')+': '+(x.content?.text||x.content?.transcription||'').substring(0,200))+'</div>').join('');
585
+ }
586
+ });
587
+ </script>
588
+ </body>
589
+ </html>`;
590
+ }
591
+ return {
592
+ contents: [
593
+ {
594
+ uri,
595
+ mimeType: "text/html",
596
+ text: htmlContent,
597
+ },
598
+ ],
599
+ };
600
+ }
601
+
546
602
  default:
547
603
  throw new Error(`Unknown resource: ${uri}`);
548
604
  }
package/ui/search.html ADDED
@@ -0,0 +1,559 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>screenpipe search</title>
7
+ <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+
14
+ body {
15
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
16
+ background: #0a0a0a;
17
+ color: #fafafa;
18
+ padding: 16px;
19
+ min-height: 100vh;
20
+ }
21
+
22
+ .container {
23
+ max-width: 700px;
24
+ margin: 0 auto;
25
+ }
26
+
27
+ .header {
28
+ display: flex;
29
+ align-items: center;
30
+ gap: 10px;
31
+ margin-bottom: 20px;
32
+ }
33
+
34
+ .header h1 {
35
+ font-size: 18px;
36
+ font-weight: 600;
37
+ }
38
+
39
+ .logo {
40
+ width: 24px;
41
+ height: 24px;
42
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
43
+ border-radius: 6px;
44
+ }
45
+
46
+ .search-box {
47
+ display: flex;
48
+ gap: 8px;
49
+ margin-bottom: 16px;
50
+ }
51
+
52
+ .search-input {
53
+ flex: 1;
54
+ padding: 12px 16px;
55
+ border: 1px solid #333;
56
+ border-radius: 8px;
57
+ background: #1a1a1a;
58
+ color: #fff;
59
+ font-size: 14px;
60
+ outline: none;
61
+ transition: border-color 0.2s;
62
+ }
63
+
64
+ .search-input:focus {
65
+ border-color: #666;
66
+ }
67
+
68
+ .search-input::placeholder {
69
+ color: #666;
70
+ }
71
+
72
+ .btn {
73
+ padding: 12px 20px;
74
+ border: none;
75
+ border-radius: 8px;
76
+ font-size: 14px;
77
+ font-weight: 500;
78
+ cursor: pointer;
79
+ transition: all 0.2s;
80
+ }
81
+
82
+ .btn-primary {
83
+ background: #fff;
84
+ color: #000;
85
+ }
86
+
87
+ .btn-primary:hover {
88
+ background: #e0e0e0;
89
+ }
90
+
91
+ .btn-secondary {
92
+ background: #1a1a1a;
93
+ color: #fff;
94
+ border: 1px solid #333;
95
+ }
96
+
97
+ .btn-secondary:hover {
98
+ background: #252525;
99
+ }
100
+
101
+ .filters {
102
+ display: flex;
103
+ gap: 8px;
104
+ margin-bottom: 20px;
105
+ flex-wrap: wrap;
106
+ }
107
+
108
+ .filter-btn {
109
+ padding: 6px 14px;
110
+ border: 1px solid #333;
111
+ border-radius: 20px;
112
+ background: transparent;
113
+ color: #888;
114
+ font-size: 13px;
115
+ cursor: pointer;
116
+ transition: all 0.2s;
117
+ }
118
+
119
+ .filter-btn:hover {
120
+ border-color: #555;
121
+ color: #fff;
122
+ }
123
+
124
+ .filter-btn.active {
125
+ background: #fff;
126
+ color: #000;
127
+ border-color: #fff;
128
+ }
129
+
130
+ .stats {
131
+ display: flex;
132
+ gap: 20px;
133
+ padding: 12px 16px;
134
+ background: #1a1a1a;
135
+ border-radius: 8px;
136
+ margin-bottom: 20px;
137
+ font-size: 13px;
138
+ }
139
+
140
+ .stat {
141
+ display: flex;
142
+ align-items: center;
143
+ gap: 6px;
144
+ }
145
+
146
+ .stat-value {
147
+ font-weight: 600;
148
+ color: #fff;
149
+ }
150
+
151
+ .stat-label {
152
+ color: #666;
153
+ }
154
+
155
+ .results {
156
+ display: flex;
157
+ flex-direction: column;
158
+ gap: 12px;
159
+ }
160
+
161
+ .result-card {
162
+ background: #1a1a1a;
163
+ border: 1px solid #252525;
164
+ border-radius: 10px;
165
+ padding: 16px;
166
+ transition: border-color 0.2s;
167
+ }
168
+
169
+ .result-card:hover {
170
+ border-color: #333;
171
+ }
172
+
173
+ .result-header {
174
+ display: flex;
175
+ justify-content: space-between;
176
+ align-items: flex-start;
177
+ margin-bottom: 10px;
178
+ }
179
+
180
+ .result-type {
181
+ display: inline-flex;
182
+ align-items: center;
183
+ gap: 6px;
184
+ padding: 4px 10px;
185
+ border-radius: 4px;
186
+ font-size: 11px;
187
+ font-weight: 600;
188
+ text-transform: uppercase;
189
+ letter-spacing: 0.5px;
190
+ }
191
+
192
+ .type-ocr {
193
+ background: rgba(59, 130, 246, 0.2);
194
+ color: #60a5fa;
195
+ }
196
+
197
+ .type-audio {
198
+ background: rgba(34, 197, 94, 0.2);
199
+ color: #4ade80;
200
+ }
201
+
202
+ .type-ui {
203
+ background: rgba(168, 85, 247, 0.2);
204
+ color: #c084fc;
205
+ }
206
+
207
+ .result-time {
208
+ font-size: 12px;
209
+ color: #666;
210
+ }
211
+
212
+ .result-app {
213
+ font-size: 13px;
214
+ color: #888;
215
+ margin-bottom: 8px;
216
+ }
217
+
218
+ .result-text {
219
+ font-size: 14px;
220
+ line-height: 1.5;
221
+ color: #ccc;
222
+ }
223
+
224
+ .result-actions {
225
+ display: flex;
226
+ gap: 8px;
227
+ margin-top: 12px;
228
+ padding-top: 12px;
229
+ border-top: 1px solid #252525;
230
+ }
231
+
232
+ .action-btn {
233
+ padding: 6px 12px;
234
+ border: 1px solid #333;
235
+ border-radius: 6px;
236
+ background: transparent;
237
+ color: #888;
238
+ font-size: 12px;
239
+ cursor: pointer;
240
+ transition: all 0.2s;
241
+ }
242
+
243
+ .action-btn:hover {
244
+ border-color: #555;
245
+ color: #fff;
246
+ }
247
+
248
+ .empty-state {
249
+ text-align: center;
250
+ padding: 60px 20px;
251
+ color: #666;
252
+ }
253
+
254
+ .empty-state h3 {
255
+ font-size: 16px;
256
+ margin-bottom: 8px;
257
+ color: #888;
258
+ }
259
+
260
+ .loading {
261
+ display: flex;
262
+ justify-content: center;
263
+ padding: 40px;
264
+ }
265
+
266
+ .spinner {
267
+ width: 24px;
268
+ height: 24px;
269
+ border: 2px solid #333;
270
+ border-top-color: #fff;
271
+ border-radius: 50%;
272
+ animation: spin 0.8s linear infinite;
273
+ }
274
+
275
+ @keyframes spin {
276
+ to { transform: rotate(360deg); }
277
+ }
278
+
279
+ .hidden {
280
+ display: none;
281
+ }
282
+ </style>
283
+ </head>
284
+ <body>
285
+ <div class="container">
286
+ <div class="header">
287
+ <div class="logo"></div>
288
+ <h1>screenpipe search</h1>
289
+ </div>
290
+
291
+ <div class="search-box">
292
+ <input
293
+ type="text"
294
+ id="query"
295
+ class="search-input"
296
+ placeholder="search your screen history, audio, and more..."
297
+ onkeydown="if(event.key==='Enter')search()"
298
+ />
299
+ <button class="btn btn-primary" onclick="search()">search</button>
300
+ </div>
301
+
302
+ <div class="filters">
303
+ <button class="filter-btn active" data-type="all" onclick="setFilter('all')">all</button>
304
+ <button class="filter-btn" data-type="ocr" onclick="setFilter('ocr')">screen</button>
305
+ <button class="filter-btn" data-type="audio" onclick="setFilter('audio')">audio</button>
306
+ <button class="filter-btn" data-type="ui" onclick="setFilter('ui')">ui elements</button>
307
+ </div>
308
+
309
+ <div id="stats" class="stats hidden">
310
+ <div class="stat">
311
+ <span class="stat-value" id="result-count">0</span>
312
+ <span class="stat-label">results</span>
313
+ </div>
314
+ <div class="stat">
315
+ <span class="stat-value" id="time-range">-</span>
316
+ <span class="stat-label">time range</span>
317
+ </div>
318
+ </div>
319
+
320
+ <div id="loading" class="loading hidden">
321
+ <div class="spinner"></div>
322
+ </div>
323
+
324
+ <div id="results" class="results">
325
+ <div class="empty-state">
326
+ <h3>search your digital history</h3>
327
+ <p>type a query above to search through your screen recordings and audio transcriptions</p>
328
+ </div>
329
+ </div>
330
+ </div>
331
+
332
+ <script>
333
+ let currentFilter = 'all';
334
+ let pendingRequestId = null;
335
+
336
+ // MCP App communication layer
337
+ const mcp = {
338
+ requestId: 0,
339
+
340
+ callTool: function(name, args) {
341
+ this.requestId++;
342
+ pendingRequestId = this.requestId;
343
+ window.parent.postMessage({
344
+ jsonrpc: '2.0',
345
+ id: this.requestId,
346
+ method: 'tools/call',
347
+ params: { name, arguments: args }
348
+ }, '*');
349
+ return this.requestId;
350
+ },
351
+
352
+ sendMessage: function(text) {
353
+ window.parent.postMessage({
354
+ jsonrpc: '2.0',
355
+ method: 'message/send',
356
+ params: { content: text }
357
+ }, '*');
358
+ }
359
+ };
360
+
361
+ // listen for messages from host
362
+ window.addEventListener('message', (event) => {
363
+ const data = event.data;
364
+
365
+ // handle tool results
366
+ if (data?.result || data?.method === 'tool/result') {
367
+ hideLoading();
368
+ const result = data.result || data.params?.result;
369
+ if (result) {
370
+ displayResults(result);
371
+ }
372
+ }
373
+
374
+ // handle errors
375
+ if (data?.error) {
376
+ hideLoading();
377
+ showError(data.error.message || 'search failed');
378
+ }
379
+ });
380
+
381
+ function setFilter(type) {
382
+ currentFilter = type;
383
+ document.querySelectorAll('.filter-btn').forEach(btn => {
384
+ btn.classList.toggle('active', btn.dataset.type === type);
385
+ });
386
+ // re-run search with new filter if there's a query
387
+ const query = document.getElementById('query').value;
388
+ if (query) {
389
+ search();
390
+ }
391
+ }
392
+
393
+ function search() {
394
+ const query = document.getElementById('query').value;
395
+ showLoading();
396
+
397
+ const args = {
398
+ q: query || undefined,
399
+ content_type: currentFilter,
400
+ limit: 20
401
+ };
402
+
403
+ mcp.callTool('search-content', args);
404
+ }
405
+
406
+ function showLoading() {
407
+ document.getElementById('loading').classList.remove('hidden');
408
+ document.getElementById('results').innerHTML = '';
409
+ document.getElementById('stats').classList.add('hidden');
410
+ }
411
+
412
+ function hideLoading() {
413
+ document.getElementById('loading').classList.add('hidden');
414
+ }
415
+
416
+ function showError(message) {
417
+ document.getElementById('results').innerHTML = `
418
+ <div class="empty-state">
419
+ <h3>error</h3>
420
+ <p>${escapeHtml(message)}</p>
421
+ </div>
422
+ `;
423
+ }
424
+
425
+ function displayResults(data) {
426
+ const results = data.data || data || [];
427
+ const container = document.getElementById('results');
428
+ const statsEl = document.getElementById('stats');
429
+
430
+ if (!Array.isArray(results) || results.length === 0) {
431
+ container.innerHTML = `
432
+ <div class="empty-state">
433
+ <h3>no results found</h3>
434
+ <p>try a different search term or adjust your filters</p>
435
+ </div>
436
+ `;
437
+ statsEl.classList.add('hidden');
438
+ return;
439
+ }
440
+
441
+ // update stats
442
+ document.getElementById('result-count').textContent = results.length;
443
+
444
+ // calculate time range
445
+ const timestamps = results
446
+ .map(r => r.content?.timestamp)
447
+ .filter(Boolean)
448
+ .map(t => new Date(t).getTime())
449
+ .sort((a, b) => a - b);
450
+
451
+ if (timestamps.length > 1) {
452
+ const start = new Date(timestamps[0]);
453
+ const end = new Date(timestamps[timestamps.length - 1]);
454
+ document.getElementById('time-range').textContent = formatTimeRange(start, end);
455
+ } else {
456
+ document.getElementById('time-range').textContent = 'now';
457
+ }
458
+
459
+ statsEl.classList.remove('hidden');
460
+
461
+ // render results
462
+ container.innerHTML = results.map(result => {
463
+ const content = result.content || {};
464
+ const type = result.type?.toLowerCase() || 'unknown';
465
+ const text = content.text || content.transcription || '';
466
+ const app = content.app_name || content.device_name || 'unknown';
467
+ const window = content.window_name || '';
468
+ const time = content.timestamp ? formatTime(new Date(content.timestamp)) : '';
469
+
470
+ return `
471
+ <div class="result-card">
472
+ <div class="result-header">
473
+ <span class="result-type type-${type}">${getTypeIcon(type)} ${type}</span>
474
+ <span class="result-time">${time}</span>
475
+ </div>
476
+ <div class="result-app">${escapeHtml(app)}${window ? ' - ' + escapeHtml(window) : ''}</div>
477
+ <div class="result-text">${escapeHtml(truncate(text, 300))}</div>
478
+ <div class="result-actions">
479
+ <button class="action-btn" onclick="copyText('${escapeJs(text)}')">copy</button>
480
+ <button class="action-btn" onclick="askAbout('${escapeJs(text.substring(0, 100))}')">ask AI</button>
481
+ </div>
482
+ </div>
483
+ `;
484
+ }).join('');
485
+ }
486
+
487
+ function getTypeIcon(type) {
488
+ switch(type) {
489
+ case 'ocr': return '';
490
+ case 'audio': return '';
491
+ case 'ui': return '';
492
+ default: return '';
493
+ }
494
+ }
495
+
496
+ function formatTime(date) {
497
+ const now = new Date();
498
+ const diff = now - date;
499
+
500
+ if (diff < 60000) return 'just now';
501
+ if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
502
+ if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
503
+
504
+ return date.toLocaleDateString('en-US', {
505
+ month: 'short',
506
+ day: 'numeric',
507
+ hour: 'numeric',
508
+ minute: '2-digit'
509
+ });
510
+ }
511
+
512
+ function formatTimeRange(start, end) {
513
+ const diff = end - start;
514
+ if (diff < 3600000) return Math.floor(diff / 60000) + ' min';
515
+ if (diff < 86400000) return Math.floor(diff / 3600000) + ' hours';
516
+ return Math.floor(diff / 86400000) + ' days';
517
+ }
518
+
519
+ function truncate(str, len) {
520
+ if (!str) return '';
521
+ if (str.length <= len) return str;
522
+ return str.substring(0, len) + '...';
523
+ }
524
+
525
+ function escapeHtml(str) {
526
+ if (!str) return '';
527
+ return str
528
+ .replace(/&/g, '&amp;')
529
+ .replace(/</g, '&lt;')
530
+ .replace(/>/g, '&gt;')
531
+ .replace(/"/g, '&quot;')
532
+ .replace(/'/g, '&#039;');
533
+ }
534
+
535
+ function escapeJs(str) {
536
+ if (!str) return '';
537
+ return str
538
+ .replace(/\\/g, '\\\\')
539
+ .replace(/'/g, "\\'")
540
+ .replace(/"/g, '\\"')
541
+ .replace(/\n/g, '\\n')
542
+ .replace(/\r/g, '\\r');
543
+ }
544
+
545
+ function copyText(text) {
546
+ navigator.clipboard.writeText(text).then(() => {
547
+ // could show a toast here
548
+ });
549
+ }
550
+
551
+ function askAbout(text) {
552
+ mcp.sendMessage(`Tell me more about: "${text}"`);
553
+ }
554
+
555
+ // auto-focus search input
556
+ document.getElementById('query').focus();
557
+ </script>
558
+ </body>
559
+ </html>