screenpipe-mcp 0.4.0 → 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/http-server.d.ts +8 -0
- package/dist/http-server.js +203 -0
- package/dist/index.js +93 -1
- package/manifest.json +5 -1
- package/package.json +3 -1
- package/src/http-server.ts +243 -0
- package/src/index.ts +93 -1
- package/ui/search.html +559 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* HTTP Server for Screenpipe MCP
|
|
5
|
+
*
|
|
6
|
+
* This allows web apps to call MCP tools over HTTP instead of stdio.
|
|
7
|
+
* Run with: npx ts-node src/http-server.ts --port 3031
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
const http_1 = require("http");
|
|
11
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
12
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
13
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
14
|
+
// Parse command line arguments
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
let mcpPort = 3031;
|
|
17
|
+
let screenpipePort = 3030;
|
|
18
|
+
for (let i = 0; i < args.length; i++) {
|
|
19
|
+
if (args[i] === "--port" && args[i + 1]) {
|
|
20
|
+
mcpPort = parseInt(args[i + 1], 10);
|
|
21
|
+
}
|
|
22
|
+
if (args[i] === "--screenpipe-port" && args[i + 1]) {
|
|
23
|
+
screenpipePort = parseInt(args[i + 1], 10);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const SCREENPIPE_API = `http://localhost:${screenpipePort}`;
|
|
27
|
+
// Tool definitions
|
|
28
|
+
const TOOLS = [
|
|
29
|
+
{
|
|
30
|
+
name: "search_content",
|
|
31
|
+
description: "Search screenpipe's recorded content: screen text (OCR), audio transcriptions, and UI elements. " +
|
|
32
|
+
"Returns timestamped results with app context. " +
|
|
33
|
+
"Call with no parameters to get recent activity.",
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
q: {
|
|
38
|
+
type: "string",
|
|
39
|
+
description: "Search query. Optional - omit to return all recent content.",
|
|
40
|
+
},
|
|
41
|
+
content_type: {
|
|
42
|
+
type: "string",
|
|
43
|
+
enum: ["all", "ocr", "audio", "ui"],
|
|
44
|
+
description: "Content type filter. Default: 'all'",
|
|
45
|
+
},
|
|
46
|
+
limit: {
|
|
47
|
+
type: "integer",
|
|
48
|
+
description: "Max results. Default: 10",
|
|
49
|
+
},
|
|
50
|
+
offset: {
|
|
51
|
+
type: "integer",
|
|
52
|
+
description: "Skip N results for pagination. Default: 0",
|
|
53
|
+
},
|
|
54
|
+
start_time: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "ISO 8601 UTC start time (e.g., 2024-01-15T10:00:00Z)",
|
|
57
|
+
},
|
|
58
|
+
end_time: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "ISO 8601 UTC end time (e.g., 2024-01-15T18:00:00Z)",
|
|
61
|
+
},
|
|
62
|
+
app_name: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "Filter by app (e.g., 'Google Chrome', 'Slack', 'zoom.us')",
|
|
65
|
+
},
|
|
66
|
+
window_name: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "Filter by window title",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
// Helper function to make HTTP requests
|
|
75
|
+
async function fetchAPI(endpoint, options = {}) {
|
|
76
|
+
const url = `${SCREENPIPE_API}${endpoint}`;
|
|
77
|
+
return fetch(url, {
|
|
78
|
+
...options,
|
|
79
|
+
headers: {
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
...options.headers,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// Create MCP server
|
|
86
|
+
const server = new index_js_1.Server({
|
|
87
|
+
name: "screenpipe-http",
|
|
88
|
+
version: "0.1.0",
|
|
89
|
+
}, {
|
|
90
|
+
capabilities: {
|
|
91
|
+
tools: {},
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
// List tools handler
|
|
95
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
96
|
+
return { tools: TOOLS };
|
|
97
|
+
});
|
|
98
|
+
// Call tool handler
|
|
99
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
100
|
+
const { name, arguments: args } = request.params;
|
|
101
|
+
if (!args) {
|
|
102
|
+
throw new Error("Missing arguments");
|
|
103
|
+
}
|
|
104
|
+
if (name === "search_content") {
|
|
105
|
+
const params = new URLSearchParams();
|
|
106
|
+
for (const [key, value] of Object.entries(args)) {
|
|
107
|
+
if (value !== null && value !== undefined) {
|
|
108
|
+
params.append(key, String(value));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const response = await fetchAPI(`/search?${params.toString()}`);
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
throw new Error(`HTTP error: ${response.status}`);
|
|
114
|
+
}
|
|
115
|
+
const data = await response.json();
|
|
116
|
+
const results = data.data || [];
|
|
117
|
+
const pagination = data.pagination || {};
|
|
118
|
+
if (results.length === 0) {
|
|
119
|
+
return {
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: "text",
|
|
123
|
+
text: "No results found. Try: broader search terms, different content_type, or wider time range.",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const formattedResults = [];
|
|
129
|
+
for (const result of results) {
|
|
130
|
+
const content = result.content;
|
|
131
|
+
if (!content)
|
|
132
|
+
continue;
|
|
133
|
+
if (result.type === "OCR") {
|
|
134
|
+
formattedResults.push(`[OCR] ${content.app_name || "?"} | ${content.window_name || "?"}\n` +
|
|
135
|
+
`${content.timestamp || ""}\n` +
|
|
136
|
+
`${content.text || ""}`);
|
|
137
|
+
}
|
|
138
|
+
else if (result.type === "Audio") {
|
|
139
|
+
formattedResults.push(`[Audio] ${content.device_name || "?"}\n` +
|
|
140
|
+
`${content.timestamp || ""}\n` +
|
|
141
|
+
`${content.transcription || ""}`);
|
|
142
|
+
}
|
|
143
|
+
else if (result.type === "UI") {
|
|
144
|
+
formattedResults.push(`[UI] ${content.app_name || "?"} | ${content.window_name || "?"}\n` +
|
|
145
|
+
`${content.timestamp || ""}\n` +
|
|
146
|
+
`${content.text || ""}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const header = `Results: ${results.length}/${pagination.total || "?"}` +
|
|
150
|
+
(pagination.total > results.length ? ` (use offset=${(pagination.offset || 0) + results.length} for more)` : "");
|
|
151
|
+
return {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: "text",
|
|
155
|
+
text: header + "\n\n" + formattedResults.join("\n---\n"),
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
161
|
+
});
|
|
162
|
+
// Create HTTP server with MCP transport
|
|
163
|
+
const transports = new Map();
|
|
164
|
+
const httpServer = (0, http_1.createServer)(async (req, res) => {
|
|
165
|
+
// CORS headers
|
|
166
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
167
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
168
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, mcp-session-id");
|
|
169
|
+
if (req.method === "OPTIONS") {
|
|
170
|
+
res.writeHead(204);
|
|
171
|
+
res.end();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Health check
|
|
175
|
+
if (req.url === "/health") {
|
|
176
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
177
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// MCP endpoint
|
|
181
|
+
if (req.url === "/mcp" || req.url?.startsWith("/mcp?")) {
|
|
182
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
183
|
+
let transport = sessionId ? transports.get(sessionId) : undefined;
|
|
184
|
+
if (!transport) {
|
|
185
|
+
transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
186
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
187
|
+
});
|
|
188
|
+
await server.connect(transport);
|
|
189
|
+
if (transport.sessionId) {
|
|
190
|
+
transports.set(transport.sessionId, transport);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
await transport.handleRequest(req, res);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
197
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
198
|
+
});
|
|
199
|
+
httpServer.listen(mcpPort, () => {
|
|
200
|
+
console.log(`Screenpipe MCP HTTP server running on http://localhost:${mcpPort}`);
|
|
201
|
+
console.log(`MCP endpoint: http://localhost:${mcpPort}/mcp`);
|
|
202
|
+
console.log(`Health check: http://localhost:${mcpPort}/health`);
|
|
203
|
+
});
|
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.
|
|
74
|
+
version: "0.5.0",
|
|
75
75
|
}, {
|
|
76
76
|
capabilities: {
|
|
77
77
|
tools: {},
|
|
@@ -87,6 +87,10 @@ const BASE_TOOLS = [
|
|
|
87
87
|
"Returns timestamped results with app context. " +
|
|
88
88
|
"Call with no parameters to get recent activity. " +
|
|
89
89
|
"Use the 'screenpipe://context' resource for current time when building time-based queries.",
|
|
90
|
+
annotations: {
|
|
91
|
+
title: "Search Content",
|
|
92
|
+
readOnlyHint: true,
|
|
93
|
+
},
|
|
90
94
|
inputSchema: {
|
|
91
95
|
type: "object",
|
|
92
96
|
properties: {
|
|
@@ -148,6 +152,10 @@ const BASE_TOOLS = [
|
|
|
148
152
|
name: "pixel-control",
|
|
149
153
|
description: "Control mouse and keyboard at the pixel level. This is a cross-platform tool that works on all operating systems. " +
|
|
150
154
|
"Use this to type text, press keys, move the mouse, and click buttons.",
|
|
155
|
+
annotations: {
|
|
156
|
+
title: "Pixel Control",
|
|
157
|
+
destructiveHint: true,
|
|
158
|
+
},
|
|
151
159
|
inputSchema: {
|
|
152
160
|
type: "object",
|
|
153
161
|
properties: {
|
|
@@ -196,6 +204,10 @@ const BASE_TOOLS = [
|
|
|
196
204
|
"EXAMPLES:\n" +
|
|
197
205
|
"- Last 30 minutes: Calculate timestamps from current time\n" +
|
|
198
206
|
"- Specific meeting: Use the meeting's start and end times in UTC",
|
|
207
|
+
annotations: {
|
|
208
|
+
title: "Export Video",
|
|
209
|
+
destructiveHint: true,
|
|
210
|
+
},
|
|
199
211
|
inputSchema: {
|
|
200
212
|
type: "object",
|
|
201
213
|
properties: {
|
|
@@ -231,6 +243,10 @@ const MACOS_TOOLS = [
|
|
|
231
243
|
"- Clickable items: 'AXButton', 'AXMenuItem', 'AXMenuBarItem', 'AXImage', 'AXStaticText'\n" +
|
|
232
244
|
"- Web content may use: 'AXWebArea', 'AXLink', 'AXHeading', 'AXRadioButton'\n\n" +
|
|
233
245
|
"Use MacOS Accessibility Inspector app to identify the exact roles in your target application.",
|
|
246
|
+
annotations: {
|
|
247
|
+
title: "Find Elements",
|
|
248
|
+
readOnlyHint: true,
|
|
249
|
+
},
|
|
234
250
|
inputSchema: {
|
|
235
251
|
type: "object",
|
|
236
252
|
properties: {
|
|
@@ -272,6 +288,10 @@ const MACOS_TOOLS = [
|
|
|
272
288
|
{
|
|
273
289
|
name: "click-element",
|
|
274
290
|
description: "Click an element in an application using its id (MacOS only)",
|
|
291
|
+
annotations: {
|
|
292
|
+
title: "Click Element",
|
|
293
|
+
destructiveHint: true,
|
|
294
|
+
},
|
|
275
295
|
inputSchema: {
|
|
276
296
|
type: "object",
|
|
277
297
|
properties: {
|
|
@@ -304,6 +324,10 @@ const MACOS_TOOLS = [
|
|
|
304
324
|
{
|
|
305
325
|
name: "fill-element",
|
|
306
326
|
description: "Type text into an element in an application (MacOS only)",
|
|
327
|
+
annotations: {
|
|
328
|
+
title: "Fill Element",
|
|
329
|
+
destructiveHint: true,
|
|
330
|
+
},
|
|
307
331
|
inputSchema: {
|
|
308
332
|
type: "object",
|
|
309
333
|
properties: {
|
|
@@ -340,6 +364,10 @@ const MACOS_TOOLS = [
|
|
|
340
364
|
{
|
|
341
365
|
name: "scroll-element",
|
|
342
366
|
description: "Scroll an element in a specific direction (MacOS only)",
|
|
367
|
+
annotations: {
|
|
368
|
+
title: "Scroll Element",
|
|
369
|
+
destructiveHint: true,
|
|
370
|
+
},
|
|
343
371
|
inputSchema: {
|
|
344
372
|
type: "object",
|
|
345
373
|
properties: {
|
|
@@ -381,6 +409,10 @@ const MACOS_TOOLS = [
|
|
|
381
409
|
{
|
|
382
410
|
name: "open-application",
|
|
383
411
|
description: "Open an application by name",
|
|
412
|
+
annotations: {
|
|
413
|
+
title: "Open Application",
|
|
414
|
+
destructiveHint: true,
|
|
415
|
+
},
|
|
384
416
|
inputSchema: {
|
|
385
417
|
type: "object",
|
|
386
418
|
properties: {
|
|
@@ -395,6 +427,10 @@ const MACOS_TOOLS = [
|
|
|
395
427
|
{
|
|
396
428
|
name: "open-url",
|
|
397
429
|
description: "Open a URL in a browser",
|
|
430
|
+
annotations: {
|
|
431
|
+
title: "Open URL",
|
|
432
|
+
destructiveHint: true,
|
|
433
|
+
},
|
|
398
434
|
inputSchema: {
|
|
399
435
|
type: "object",
|
|
400
436
|
properties: {
|
|
@@ -433,6 +469,12 @@ const RESOURCES = [
|
|
|
433
469
|
description: "How to use screenpipe search effectively",
|
|
434
470
|
mimeType: "text/markdown",
|
|
435
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
|
+
},
|
|
436
478
|
];
|
|
437
479
|
// List resources handler
|
|
438
480
|
server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
|
|
@@ -505,6 +547,56 @@ server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) =
|
|
|
505
547
|
},
|
|
506
548
|
],
|
|
507
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
|
+
}
|
|
508
600
|
default:
|
|
509
601
|
throw new Error(`Unknown resource: ${uri}`);
|
|
510
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.
|
|
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.
|
|
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": {
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"start": "node dist/index.js",
|
|
12
|
+
"start:http": "node dist/http-server.js",
|
|
12
13
|
"dev": "ts-node src/index.ts",
|
|
14
|
+
"dev:http": "ts-node src/http-server.ts",
|
|
13
15
|
"test": "vitest run",
|
|
14
16
|
"test:watch": "vitest",
|
|
15
17
|
"prepublishOnly": "npm run build"
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HTTP Server for Screenpipe MCP
|
|
5
|
+
*
|
|
6
|
+
* This allows web apps to call MCP tools over HTTP instead of stdio.
|
|
7
|
+
* Run with: npx ts-node src/http-server.ts --port 3031
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createServer } from "http";
|
|
11
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
12
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
13
|
+
import {
|
|
14
|
+
CallToolRequestSchema,
|
|
15
|
+
ListToolsRequestSchema,
|
|
16
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
+
|
|
18
|
+
// Parse command line arguments
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
let mcpPort = 3031;
|
|
21
|
+
let screenpipePort = 3030;
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < args.length; i++) {
|
|
24
|
+
if (args[i] === "--port" && args[i + 1]) {
|
|
25
|
+
mcpPort = parseInt(args[i + 1], 10);
|
|
26
|
+
}
|
|
27
|
+
if (args[i] === "--screenpipe-port" && args[i + 1]) {
|
|
28
|
+
screenpipePort = parseInt(args[i + 1], 10);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const SCREENPIPE_API = `http://localhost:${screenpipePort}`;
|
|
33
|
+
|
|
34
|
+
// Tool definitions
|
|
35
|
+
const TOOLS = [
|
|
36
|
+
{
|
|
37
|
+
name: "search_content",
|
|
38
|
+
description:
|
|
39
|
+
"Search screenpipe's recorded content: screen text (OCR), audio transcriptions, and UI elements. " +
|
|
40
|
+
"Returns timestamped results with app context. " +
|
|
41
|
+
"Call with no parameters to get recent activity.",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: "object" as const,
|
|
44
|
+
properties: {
|
|
45
|
+
q: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "Search query. Optional - omit to return all recent content.",
|
|
48
|
+
},
|
|
49
|
+
content_type: {
|
|
50
|
+
type: "string",
|
|
51
|
+
enum: ["all", "ocr", "audio", "ui"],
|
|
52
|
+
description: "Content type filter. Default: 'all'",
|
|
53
|
+
},
|
|
54
|
+
limit: {
|
|
55
|
+
type: "integer",
|
|
56
|
+
description: "Max results. Default: 10",
|
|
57
|
+
},
|
|
58
|
+
offset: {
|
|
59
|
+
type: "integer",
|
|
60
|
+
description: "Skip N results for pagination. Default: 0",
|
|
61
|
+
},
|
|
62
|
+
start_time: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "ISO 8601 UTC start time (e.g., 2024-01-15T10:00:00Z)",
|
|
65
|
+
},
|
|
66
|
+
end_time: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "ISO 8601 UTC end time (e.g., 2024-01-15T18:00:00Z)",
|
|
69
|
+
},
|
|
70
|
+
app_name: {
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "Filter by app (e.g., 'Google Chrome', 'Slack', 'zoom.us')",
|
|
73
|
+
},
|
|
74
|
+
window_name: {
|
|
75
|
+
type: "string",
|
|
76
|
+
description: "Filter by window title",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// Helper function to make HTTP requests
|
|
84
|
+
async function fetchAPI(endpoint: string, options: RequestInit = {}): Promise<Response> {
|
|
85
|
+
const url = `${SCREENPIPE_API}${endpoint}`;
|
|
86
|
+
return fetch(url, {
|
|
87
|
+
...options,
|
|
88
|
+
headers: {
|
|
89
|
+
"Content-Type": "application/json",
|
|
90
|
+
...options.headers,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Create MCP server
|
|
96
|
+
const server = new Server(
|
|
97
|
+
{
|
|
98
|
+
name: "screenpipe-http",
|
|
99
|
+
version: "0.1.0",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
capabilities: {
|
|
103
|
+
tools: {},
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// List tools handler
|
|
109
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
110
|
+
return { tools: TOOLS };
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Call tool handler
|
|
114
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
115
|
+
const { name, arguments: args } = request.params;
|
|
116
|
+
|
|
117
|
+
if (!args) {
|
|
118
|
+
throw new Error("Missing arguments");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (name === "search_content") {
|
|
122
|
+
const params = new URLSearchParams();
|
|
123
|
+
for (const [key, value] of Object.entries(args)) {
|
|
124
|
+
if (value !== null && value !== undefined) {
|
|
125
|
+
params.append(key, String(value));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const response = await fetchAPI(`/search?${params.toString()}`);
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
throw new Error(`HTTP error: ${response.status}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const data = await response.json();
|
|
135
|
+
const results = data.data || [];
|
|
136
|
+
const pagination = data.pagination || {};
|
|
137
|
+
|
|
138
|
+
if (results.length === 0) {
|
|
139
|
+
return {
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
type: "text",
|
|
143
|
+
text: "No results found. Try: broader search terms, different content_type, or wider time range.",
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const formattedResults: string[] = [];
|
|
150
|
+
for (const result of results) {
|
|
151
|
+
const content = result.content;
|
|
152
|
+
if (!content) continue;
|
|
153
|
+
|
|
154
|
+
if (result.type === "OCR") {
|
|
155
|
+
formattedResults.push(
|
|
156
|
+
`[OCR] ${content.app_name || "?"} | ${content.window_name || "?"}\n` +
|
|
157
|
+
`${content.timestamp || ""}\n` +
|
|
158
|
+
`${content.text || ""}`
|
|
159
|
+
);
|
|
160
|
+
} else if (result.type === "Audio") {
|
|
161
|
+
formattedResults.push(
|
|
162
|
+
`[Audio] ${content.device_name || "?"}\n` +
|
|
163
|
+
`${content.timestamp || ""}\n` +
|
|
164
|
+
`${content.transcription || ""}`
|
|
165
|
+
);
|
|
166
|
+
} else if (result.type === "UI") {
|
|
167
|
+
formattedResults.push(
|
|
168
|
+
`[UI] ${content.app_name || "?"} | ${content.window_name || "?"}\n` +
|
|
169
|
+
`${content.timestamp || ""}\n` +
|
|
170
|
+
`${content.text || ""}`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const header = `Results: ${results.length}/${pagination.total || "?"}` +
|
|
176
|
+
(pagination.total > results.length ? ` (use offset=${(pagination.offset || 0) + results.length} for more)` : "");
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
content: [
|
|
180
|
+
{
|
|
181
|
+
type: "text",
|
|
182
|
+
text: header + "\n\n" + formattedResults.join("\n---\n"),
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Create HTTP server with MCP transport
|
|
192
|
+
const transports = new Map<string, StreamableHTTPServerTransport>();
|
|
193
|
+
|
|
194
|
+
const httpServer = createServer(async (req, res) => {
|
|
195
|
+
// CORS headers
|
|
196
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
197
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
198
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, mcp-session-id");
|
|
199
|
+
|
|
200
|
+
if (req.method === "OPTIONS") {
|
|
201
|
+
res.writeHead(204);
|
|
202
|
+
res.end();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Health check
|
|
207
|
+
if (req.url === "/health") {
|
|
208
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
209
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// MCP endpoint
|
|
214
|
+
if (req.url === "/mcp" || req.url?.startsWith("/mcp?")) {
|
|
215
|
+
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
216
|
+
|
|
217
|
+
let transport = sessionId ? transports.get(sessionId) : undefined;
|
|
218
|
+
|
|
219
|
+
if (!transport) {
|
|
220
|
+
transport = new StreamableHTTPServerTransport({
|
|
221
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await server.connect(transport);
|
|
225
|
+
|
|
226
|
+
if (transport.sessionId) {
|
|
227
|
+
transports.set(transport.sessionId, transport);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await transport.handleRequest(req, res);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
236
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
httpServer.listen(mcpPort, () => {
|
|
240
|
+
console.log(`Screenpipe MCP HTTP server running on http://localhost:${mcpPort}`);
|
|
241
|
+
console.log(`MCP endpoint: http://localhost:${mcpPort}/mcp`);
|
|
242
|
+
console.log(`Health check: http://localhost:${mcpPort}/health`);
|
|
243
|
+
});
|
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.
|
|
54
|
+
version: "0.5.0",
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
capabilities: {
|
|
@@ -71,6 +71,10 @@ const BASE_TOOLS: Tool[] = [
|
|
|
71
71
|
"Returns timestamped results with app context. " +
|
|
72
72
|
"Call with no parameters to get recent activity. " +
|
|
73
73
|
"Use the 'screenpipe://context' resource for current time when building time-based queries.",
|
|
74
|
+
annotations: {
|
|
75
|
+
title: "Search Content",
|
|
76
|
+
readOnlyHint: true,
|
|
77
|
+
},
|
|
74
78
|
inputSchema: {
|
|
75
79
|
type: "object",
|
|
76
80
|
properties: {
|
|
@@ -133,6 +137,10 @@ const BASE_TOOLS: Tool[] = [
|
|
|
133
137
|
description:
|
|
134
138
|
"Control mouse and keyboard at the pixel level. This is a cross-platform tool that works on all operating systems. " +
|
|
135
139
|
"Use this to type text, press keys, move the mouse, and click buttons.",
|
|
140
|
+
annotations: {
|
|
141
|
+
title: "Pixel Control",
|
|
142
|
+
destructiveHint: true,
|
|
143
|
+
},
|
|
136
144
|
inputSchema: {
|
|
137
145
|
type: "object",
|
|
138
146
|
properties: {
|
|
@@ -183,6 +191,10 @@ const BASE_TOOLS: Tool[] = [
|
|
|
183
191
|
"EXAMPLES:\n" +
|
|
184
192
|
"- Last 30 minutes: Calculate timestamps from current time\n" +
|
|
185
193
|
"- Specific meeting: Use the meeting's start and end times in UTC",
|
|
194
|
+
annotations: {
|
|
195
|
+
title: "Export Video",
|
|
196
|
+
destructiveHint: true,
|
|
197
|
+
},
|
|
186
198
|
inputSchema: {
|
|
187
199
|
type: "object",
|
|
188
200
|
properties: {
|
|
@@ -223,6 +235,10 @@ const MACOS_TOOLS: Tool[] = [
|
|
|
223
235
|
"- Clickable items: 'AXButton', 'AXMenuItem', 'AXMenuBarItem', 'AXImage', 'AXStaticText'\n" +
|
|
224
236
|
"- Web content may use: 'AXWebArea', 'AXLink', 'AXHeading', 'AXRadioButton'\n\n" +
|
|
225
237
|
"Use MacOS Accessibility Inspector app to identify the exact roles in your target application.",
|
|
238
|
+
annotations: {
|
|
239
|
+
title: "Find Elements",
|
|
240
|
+
readOnlyHint: true,
|
|
241
|
+
},
|
|
226
242
|
inputSchema: {
|
|
227
243
|
type: "object",
|
|
228
244
|
properties: {
|
|
@@ -267,6 +283,10 @@ const MACOS_TOOLS: Tool[] = [
|
|
|
267
283
|
name: "click-element",
|
|
268
284
|
description:
|
|
269
285
|
"Click an element in an application using its id (MacOS only)",
|
|
286
|
+
annotations: {
|
|
287
|
+
title: "Click Element",
|
|
288
|
+
destructiveHint: true,
|
|
289
|
+
},
|
|
270
290
|
inputSchema: {
|
|
271
291
|
type: "object",
|
|
272
292
|
properties: {
|
|
@@ -299,6 +319,10 @@ const MACOS_TOOLS: Tool[] = [
|
|
|
299
319
|
{
|
|
300
320
|
name: "fill-element",
|
|
301
321
|
description: "Type text into an element in an application (MacOS only)",
|
|
322
|
+
annotations: {
|
|
323
|
+
title: "Fill Element",
|
|
324
|
+
destructiveHint: true,
|
|
325
|
+
},
|
|
302
326
|
inputSchema: {
|
|
303
327
|
type: "object",
|
|
304
328
|
properties: {
|
|
@@ -335,6 +359,10 @@ const MACOS_TOOLS: Tool[] = [
|
|
|
335
359
|
{
|
|
336
360
|
name: "scroll-element",
|
|
337
361
|
description: "Scroll an element in a specific direction (MacOS only)",
|
|
362
|
+
annotations: {
|
|
363
|
+
title: "Scroll Element",
|
|
364
|
+
destructiveHint: true,
|
|
365
|
+
},
|
|
338
366
|
inputSchema: {
|
|
339
367
|
type: "object",
|
|
340
368
|
properties: {
|
|
@@ -376,6 +404,10 @@ const MACOS_TOOLS: Tool[] = [
|
|
|
376
404
|
{
|
|
377
405
|
name: "open-application",
|
|
378
406
|
description: "Open an application by name",
|
|
407
|
+
annotations: {
|
|
408
|
+
title: "Open Application",
|
|
409
|
+
destructiveHint: true,
|
|
410
|
+
},
|
|
379
411
|
inputSchema: {
|
|
380
412
|
type: "object",
|
|
381
413
|
properties: {
|
|
@@ -390,6 +422,10 @@ const MACOS_TOOLS: Tool[] = [
|
|
|
390
422
|
{
|
|
391
423
|
name: "open-url",
|
|
392
424
|
description: "Open a URL in a browser",
|
|
425
|
+
annotations: {
|
|
426
|
+
title: "Open URL",
|
|
427
|
+
destructiveHint: true,
|
|
428
|
+
},
|
|
393
429
|
inputSchema: {
|
|
394
430
|
type: "object",
|
|
395
431
|
properties: {
|
|
@@ -430,6 +466,12 @@ const RESOURCES = [
|
|
|
430
466
|
description: "How to use screenpipe search effectively",
|
|
431
467
|
mimeType: "text/markdown",
|
|
432
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
|
+
},
|
|
433
475
|
];
|
|
434
476
|
|
|
435
477
|
// List resources handler
|
|
@@ -507,6 +549,56 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
507
549
|
],
|
|
508
550
|
};
|
|
509
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
|
+
|
|
510
602
|
default:
|
|
511
603
|
throw new Error(`Unknown resource: ${uri}`);
|
|
512
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, '&')
|
|
529
|
+
.replace(/</g, '<')
|
|
530
|
+
.replace(/>/g, '>')
|
|
531
|
+
.replace(/"/g, '"')
|
|
532
|
+
.replace(/'/g, ''');
|
|
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>
|