screenpipe-mcp 0.3.1 → 0.4.1
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/export-video.test.d.ts +1 -0
- package/dist/export-video.test.js +348 -0
- package/dist/http-server.d.ts +8 -0
- package/dist/http-server.js +203 -0
- package/dist/index.js +504 -53
- package/package.json +10 -3
- package/src/export-video.test.ts +370 -0
- package/src/http-server.ts +243 -0
- package/src/index.ts +520 -61
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const vitest_1 = require("vitest");
|
|
37
|
+
const ws_1 = __importStar(require("ws"));
|
|
38
|
+
const http = __importStar(require("http"));
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
// Mock the search API response
|
|
43
|
+
function createMockSearchResponse(frameIds) {
|
|
44
|
+
return {
|
|
45
|
+
data: frameIds.map((id, index) => ({
|
|
46
|
+
type: "OCR",
|
|
47
|
+
content: {
|
|
48
|
+
frame_id: id,
|
|
49
|
+
text: `Screen content ${index}`,
|
|
50
|
+
timestamp: new Date(Date.now() - (frameIds.length - index) * 60000).toISOString(),
|
|
51
|
+
app_name: "Test App",
|
|
52
|
+
window_name: "Test Window",
|
|
53
|
+
},
|
|
54
|
+
})),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// Mock video export progress messages
|
|
58
|
+
function createExportProgressMessages(frameCount) {
|
|
59
|
+
const messages = [];
|
|
60
|
+
// Extracting phase
|
|
61
|
+
for (let i = 0; i <= 10; i++) {
|
|
62
|
+
messages.push(JSON.stringify({
|
|
63
|
+
status: "extracting",
|
|
64
|
+
progress: (i / 10) * 0.5,
|
|
65
|
+
video_data: null,
|
|
66
|
+
error: null,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
// Encoding phase
|
|
70
|
+
for (let i = 0; i <= 10; i++) {
|
|
71
|
+
messages.push(JSON.stringify({
|
|
72
|
+
status: "encoding",
|
|
73
|
+
progress: 0.5 + (i / 10) * 0.5,
|
|
74
|
+
video_data: null,
|
|
75
|
+
error: null,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
return messages;
|
|
79
|
+
}
|
|
80
|
+
(0, vitest_1.describe)("export-video MCP tool", () => {
|
|
81
|
+
let mockHttpServer;
|
|
82
|
+
let mockWsServer;
|
|
83
|
+
let serverPort;
|
|
84
|
+
(0, vitest_1.beforeEach)(async () => {
|
|
85
|
+
// Create a mock HTTP server for the search API
|
|
86
|
+
mockHttpServer = http.createServer((req, res) => {
|
|
87
|
+
const url = new URL(req.url, `http://localhost`);
|
|
88
|
+
if (url.pathname === "/search") {
|
|
89
|
+
const startTime = url.searchParams.get("start_time");
|
|
90
|
+
const endTime = url.searchParams.get("end_time");
|
|
91
|
+
const contentType = url.searchParams.get("content_type");
|
|
92
|
+
// Validate required parameters
|
|
93
|
+
if (!startTime || !endTime) {
|
|
94
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
95
|
+
res.end(JSON.stringify({ error: "Missing time parameters" }));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Return mock search results with frame IDs
|
|
99
|
+
const mockResponse = createMockSearchResponse([100, 101, 102, 103, 104]);
|
|
100
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
101
|
+
res.end(JSON.stringify(mockResponse));
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
res.writeHead(404);
|
|
105
|
+
res.end();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
// Create WebSocket server for export endpoint
|
|
109
|
+
mockWsServer = new ws_1.WebSocketServer({ noServer: true });
|
|
110
|
+
mockHttpServer.on("upgrade", (request, socket, head) => {
|
|
111
|
+
const url = new URL(request.url, `http://localhost`);
|
|
112
|
+
if (url.pathname === "/frames/export") {
|
|
113
|
+
mockWsServer.handleUpgrade(request, socket, head, (ws) => {
|
|
114
|
+
// Send progress updates
|
|
115
|
+
const progressMessages = createExportProgressMessages(5);
|
|
116
|
+
let messageIndex = 0;
|
|
117
|
+
const sendProgress = setInterval(() => {
|
|
118
|
+
if (messageIndex < progressMessages.length) {
|
|
119
|
+
ws.send(progressMessages[messageIndex]);
|
|
120
|
+
messageIndex++;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
clearInterval(sendProgress);
|
|
124
|
+
// Send completed message with mock video data
|
|
125
|
+
const mockVideoData = Buffer.from("mock video content for testing");
|
|
126
|
+
ws.send(JSON.stringify({
|
|
127
|
+
status: "completed",
|
|
128
|
+
progress: 1.0,
|
|
129
|
+
video_data: Array.from(mockVideoData),
|
|
130
|
+
error: null,
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
}, 10);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
// Start the server on a random port
|
|
138
|
+
await new Promise((resolve) => {
|
|
139
|
+
mockHttpServer.listen(0, () => {
|
|
140
|
+
const address = mockHttpServer.address();
|
|
141
|
+
serverPort = address.port;
|
|
142
|
+
resolve();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
(0, vitest_1.afterEach)(async () => {
|
|
147
|
+
mockWsServer.close();
|
|
148
|
+
await new Promise((resolve) => {
|
|
149
|
+
mockHttpServer.close(() => resolve());
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
(0, vitest_1.it)("should parse ISO 8601 timestamps correctly", () => {
|
|
153
|
+
const startTime = "2024-01-15T10:00:00Z";
|
|
154
|
+
const endTime = "2024-01-15T10:30:00Z";
|
|
155
|
+
const startDate = new Date(startTime);
|
|
156
|
+
const endDate = new Date(endTime);
|
|
157
|
+
(0, vitest_1.expect)(startDate.getTime()).toBeLessThan(endDate.getTime());
|
|
158
|
+
(0, vitest_1.expect)(endDate.getTime() - startDate.getTime()).toBe(30 * 60 * 1000); // 30 minutes
|
|
159
|
+
});
|
|
160
|
+
(0, vitest_1.it)("should extract unique frame IDs from search results", () => {
|
|
161
|
+
const searchResults = createMockSearchResponse([100, 101, 100, 102, 101, 103]);
|
|
162
|
+
const frameIds = [];
|
|
163
|
+
const seenIds = new Set();
|
|
164
|
+
for (const result of searchResults.data) {
|
|
165
|
+
if (result.type === "OCR" && result.content?.frame_id) {
|
|
166
|
+
const frameId = result.content.frame_id;
|
|
167
|
+
if (!seenIds.has(frameId)) {
|
|
168
|
+
seenIds.add(frameId);
|
|
169
|
+
frameIds.push(frameId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
(0, vitest_1.expect)(frameIds).toEqual([100, 101, 102, 103]);
|
|
174
|
+
(0, vitest_1.expect)(frameIds.length).toBe(4);
|
|
175
|
+
});
|
|
176
|
+
(0, vitest_1.it)("should handle empty search results", () => {
|
|
177
|
+
const emptyResults = { data: [] };
|
|
178
|
+
const frameIds = [];
|
|
179
|
+
for (const result of emptyResults.data) {
|
|
180
|
+
if (result.type === "OCR" && result.content?.frame_id) {
|
|
181
|
+
frameIds.push(result.content.frame_id);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
(0, vitest_1.expect)(frameIds.length).toBe(0);
|
|
185
|
+
});
|
|
186
|
+
(0, vitest_1.it)("should build correct WebSocket URL with frame IDs", () => {
|
|
187
|
+
const frameIds = [100, 101, 102];
|
|
188
|
+
const fps = 1.0;
|
|
189
|
+
const port = 3030;
|
|
190
|
+
const wsUrl = `ws://localhost:${port}/frames/export?frame_ids=${frameIds.join(",")}&fps=${fps}`;
|
|
191
|
+
(0, vitest_1.expect)(wsUrl).toBe("ws://localhost:3030/frames/export?frame_ids=100,101,102&fps=1");
|
|
192
|
+
});
|
|
193
|
+
(0, vitest_1.it)("should connect to mock WebSocket server and receive messages", async () => {
|
|
194
|
+
const wsUrl = `ws://localhost:${serverPort}/frames/export?frame_ids=100,101,102&fps=1`;
|
|
195
|
+
const result = await new Promise((resolve) => {
|
|
196
|
+
const ws = new ws_1.default(wsUrl);
|
|
197
|
+
let lastMessage;
|
|
198
|
+
ws.on("error", (error) => {
|
|
199
|
+
resolve({ success: false, error: error.message });
|
|
200
|
+
});
|
|
201
|
+
ws.on("message", (data) => {
|
|
202
|
+
try {
|
|
203
|
+
lastMessage = JSON.parse(data.toString());
|
|
204
|
+
if (lastMessage.status === "completed") {
|
|
205
|
+
ws.close();
|
|
206
|
+
resolve({ success: true, data: lastMessage });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (e) {
|
|
210
|
+
// Ignore parse errors
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
ws.on("close", () => {
|
|
214
|
+
if (!lastMessage || lastMessage.status !== "completed") {
|
|
215
|
+
resolve({ success: false, error: "Connection closed before completion" });
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
// Timeout after 5 seconds
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
ws.close();
|
|
221
|
+
resolve({ success: false, error: "Timeout" });
|
|
222
|
+
}, 5000);
|
|
223
|
+
});
|
|
224
|
+
(0, vitest_1.expect)(result.success).toBe(true);
|
|
225
|
+
(0, vitest_1.expect)(result.data).toBeDefined();
|
|
226
|
+
(0, vitest_1.expect)(result.data.status).toBe("completed");
|
|
227
|
+
(0, vitest_1.expect)(result.data.video_data).toBeDefined();
|
|
228
|
+
(0, vitest_1.expect)(Array.isArray(result.data.video_data)).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
(0, vitest_1.it)("should save video data to temp file", () => {
|
|
231
|
+
const mockVideoData = Buffer.from("mock video content");
|
|
232
|
+
const tempDir = os.tmpdir();
|
|
233
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
234
|
+
const filename = `screenpipe_export_test_${timestamp}.mp4`;
|
|
235
|
+
const filePath = path.join(tempDir, filename);
|
|
236
|
+
fs.writeFileSync(filePath, mockVideoData);
|
|
237
|
+
(0, vitest_1.expect)(fs.existsSync(filePath)).toBe(true);
|
|
238
|
+
const readData = fs.readFileSync(filePath);
|
|
239
|
+
(0, vitest_1.expect)(readData.toString()).toBe("mock video content");
|
|
240
|
+
// Cleanup
|
|
241
|
+
fs.unlinkSync(filePath);
|
|
242
|
+
});
|
|
243
|
+
(0, vitest_1.it)("should sort frame IDs in ascending order", () => {
|
|
244
|
+
const unsortedIds = [103, 100, 105, 101, 102];
|
|
245
|
+
const sortedIds = [...unsortedIds].sort((a, b) => a - b);
|
|
246
|
+
(0, vitest_1.expect)(sortedIds).toEqual([100, 101, 102, 103, 105]);
|
|
247
|
+
});
|
|
248
|
+
(0, vitest_1.it)("should handle search API errors gracefully", async () => {
|
|
249
|
+
// Create a server that returns an error
|
|
250
|
+
const errorServer = http.createServer((req, res) => {
|
|
251
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
252
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
253
|
+
});
|
|
254
|
+
await new Promise((resolve) => {
|
|
255
|
+
errorServer.listen(0, () => resolve());
|
|
256
|
+
});
|
|
257
|
+
const address = errorServer.address();
|
|
258
|
+
const port = address.port;
|
|
259
|
+
try {
|
|
260
|
+
const response = await fetch(`http://localhost:${port}/search?start_time=2024-01-01T00:00:00Z&end_time=2024-01-01T01:00:00Z`);
|
|
261
|
+
(0, vitest_1.expect)(response.ok).toBe(false);
|
|
262
|
+
(0, vitest_1.expect)(response.status).toBe(500);
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
await new Promise((resolve) => {
|
|
266
|
+
errorServer.close(() => resolve());
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
(0, vitest_1.it)("should handle WebSocket connection errors", async () => {
|
|
271
|
+
const ws = new ws_1.default("ws://localhost:59999/invalid"); // Port that's not listening
|
|
272
|
+
const result = await new Promise((resolve) => {
|
|
273
|
+
ws.on("open", () => {
|
|
274
|
+
resolve({ connected: true });
|
|
275
|
+
});
|
|
276
|
+
ws.on("error", (error) => {
|
|
277
|
+
resolve({ connected: false, error: error.message });
|
|
278
|
+
});
|
|
279
|
+
setTimeout(() => {
|
|
280
|
+
resolve({ connected: false, error: "Timeout" });
|
|
281
|
+
}, 2000);
|
|
282
|
+
});
|
|
283
|
+
(0, vitest_1.expect)(result.connected).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
(0, vitest_1.it)("should validate time range parameters", () => {
|
|
286
|
+
const startTime = "2024-01-15T10:30:00Z";
|
|
287
|
+
const endTime = "2024-01-15T10:00:00Z"; // End before start
|
|
288
|
+
const startDate = new Date(startTime);
|
|
289
|
+
const endDate = new Date(endTime);
|
|
290
|
+
// This should be invalid (end before start)
|
|
291
|
+
(0, vitest_1.expect)(endDate.getTime()).toBeLessThan(startDate.getTime());
|
|
292
|
+
});
|
|
293
|
+
(0, vitest_1.it)("should handle audio-only results (no frame IDs)", () => {
|
|
294
|
+
const audioOnlyResults = {
|
|
295
|
+
data: [
|
|
296
|
+
{
|
|
297
|
+
type: "Audio",
|
|
298
|
+
content: {
|
|
299
|
+
transcription: "Hello world",
|
|
300
|
+
timestamp: "2024-01-15T10:00:00Z",
|
|
301
|
+
device_name: "Microphone",
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
type: "Audio",
|
|
306
|
+
content: {
|
|
307
|
+
transcription: "How are you",
|
|
308
|
+
timestamp: "2024-01-15T10:01:00Z",
|
|
309
|
+
device_name: "Microphone",
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
};
|
|
314
|
+
const frameIds = [];
|
|
315
|
+
for (const result of audioOnlyResults.data) {
|
|
316
|
+
if (result.type === "OCR" && result.content?.frame_id) {
|
|
317
|
+
frameIds.push(result.content.frame_id);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
(0, vitest_1.expect)(frameIds.length).toBe(0);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
(0, vitest_1.describe)("export-video tool schema validation", () => {
|
|
324
|
+
(0, vitest_1.it)("should have correct input schema", () => {
|
|
325
|
+
const schema = {
|
|
326
|
+
type: "object",
|
|
327
|
+
properties: {
|
|
328
|
+
start_time: {
|
|
329
|
+
type: "string",
|
|
330
|
+
format: "date-time",
|
|
331
|
+
},
|
|
332
|
+
end_time: {
|
|
333
|
+
type: "string",
|
|
334
|
+
format: "date-time",
|
|
335
|
+
},
|
|
336
|
+
fps: {
|
|
337
|
+
type: "number",
|
|
338
|
+
default: 1.0,
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
required: ["start_time", "end_time"],
|
|
342
|
+
};
|
|
343
|
+
(0, vitest_1.expect)(schema.required).toContain("start_time");
|
|
344
|
+
(0, vitest_1.expect)(schema.required).toContain("end_time");
|
|
345
|
+
(0, vitest_1.expect)(schema.required).not.toContain("fps"); // fps is optional
|
|
346
|
+
(0, vitest_1.expect)(schema.properties.fps.default).toBe(1.0);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
@@ -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
|
+
});
|