screenpipe-mcp 0.18.9 → 0.18.11
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/LICENSE.md +90 -0
- package/README.md +1 -0
- package/bun.lock +0 -10
- package/dist/export-video.test.js +79 -271
- package/dist/index.js +48 -103
- package/manifest.json +1 -1
- package/package.json +3 -5
- package/src/export-video.test.ts +89 -317
- package/src/index.ts +57 -120
package/dist/index.js
CHANGED
|
@@ -40,7 +40,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
40
40
|
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
41
41
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
42
42
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
43
|
-
const ws_1 = require("ws");
|
|
44
43
|
const fs = __importStar(require("fs"));
|
|
45
44
|
const path = __importStar(require("path"));
|
|
46
45
|
const os = __importStar(require("os"));
|
|
@@ -316,6 +315,10 @@ const TOOLS = [
|
|
|
316
315
|
},
|
|
317
316
|
speaker_ids: { type: "string", description: "Comma-separated speaker IDs to filter audio" },
|
|
318
317
|
speaker_name: { type: "string", description: "Filter audio by speaker name (case-insensitive partial match)" },
|
|
318
|
+
tags: {
|
|
319
|
+
type: "string",
|
|
320
|
+
description: "Comma-separated tags; returns only items carrying ALL of them (e.g. 'person:ada,project:atlas'). Works for screen + audio (content_type 'ocr'/'audio'/'all', tags written by add-tags) AND memories (content_type 'memory', tags written by update-memory). Same tag string links across all three, so two items sharing a tag are connected. Use namespaced tags (person:, project:, topic:) to link people/projects/topics. content_type 'input' and 'accessibility' have no tags and return nothing when this is set.",
|
|
321
|
+
},
|
|
319
322
|
max_content_length: {
|
|
320
323
|
type: "integer",
|
|
321
324
|
description: "Truncate each result's text via middle-truncation. Use 200-500 to keep responses compact.",
|
|
@@ -397,15 +400,21 @@ const TOOLS = [
|
|
|
397
400
|
},
|
|
398
401
|
{
|
|
399
402
|
name: "export-video",
|
|
400
|
-
description: "Export an MP4
|
|
401
|
-
"
|
|
403
|
+
description: "Export an MP4 of screen recordings for a time range, with synced microphone audio. " +
|
|
404
|
+
"Frames are placed at their real timestamps, so the clip's duration matches the " +
|
|
405
|
+
"wall-clock span you requested (not a sped-up timelapse). Returns the file path. " +
|
|
406
|
+
"Can take a few minutes for long ranges.",
|
|
402
407
|
annotations: { title: "Export Video", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
403
408
|
inputSchema: {
|
|
404
409
|
type: "object",
|
|
405
410
|
properties: {
|
|
406
|
-
start_time: { type: "string", description:
|
|
407
|
-
end_time: { type: "string", description:
|
|
408
|
-
|
|
411
|
+
start_time: { type: "string", description: 'ISO 8601 UTC or relative (e.g. "5m ago", "now")' },
|
|
412
|
+
end_time: { type: "string", description: 'ISO 8601 UTC or relative (e.g. "5m ago", "now")' },
|
|
413
|
+
output_path: {
|
|
414
|
+
type: "string",
|
|
415
|
+
description: "Optional absolute path for the MP4 (e.g. ~/Downloads/clip.mp4). " +
|
|
416
|
+
"Defaults to the screenpipe data dir's exports/ folder.",
|
|
417
|
+
},
|
|
409
418
|
},
|
|
410
419
|
required: ["start_time", "end_time"],
|
|
411
420
|
},
|
|
@@ -421,7 +430,7 @@ const TOOLS = [
|
|
|
421
430
|
properties: {
|
|
422
431
|
id: { type: "integer", description: "Memory ID — omit to create new, provide to update/delete" },
|
|
423
432
|
content: { type: "string", description: "Memory text (required for creation)" },
|
|
424
|
-
tags: { type: "array", items: { type: "string" }, description: "
|
|
433
|
+
tags: { type: "array", items: { type: "string" }, description: "Tags. Prefer namespaced (person:ada, project:atlas, topic:pricing) so this memory links to the same people/projects you tag on frames/audio. Retrieve with search-content content_type='memory' tags='person:ada'." },
|
|
425
434
|
importance: { type: "number", description: "0.0 (trivial) to 1.0 (critical). Default 0.5." },
|
|
426
435
|
source_context: { type: "object", description: "Optional metadata linking to source (app, timestamp, etc.)" },
|
|
427
436
|
delete: { type: "boolean", description: "Set true to delete the memory identified by id" },
|
|
@@ -481,14 +490,18 @@ const TOOLS = [
|
|
|
481
490
|
},
|
|
482
491
|
{
|
|
483
492
|
name: "add-tags",
|
|
484
|
-
description: "
|
|
493
|
+
description: "Tag a screen frame (vision) or audio chunk (audio) so it can be retrieved later. " +
|
|
494
|
+
"Tags are a shared linking layer: use namespaced tags (person:ada, project:atlas, topic:pricing) to connect a capture to a person, project, or topic. " +
|
|
495
|
+
"The SAME tag string also works on memories (via update-memory), so tagging a frame and a memory with person:ada links them. " +
|
|
496
|
+
"Retrieve later with search-content tags='person:ada' (add content_type+start_time/end_time to scope to a timeframe). " +
|
|
497
|
+
"Note: frames are pruned by retention, so for durable links prefer tagging a memory; tag frames/audio for shorter-term recall.",
|
|
485
498
|
annotations: { title: "Add Tags", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
486
499
|
inputSchema: {
|
|
487
500
|
type: "object",
|
|
488
501
|
properties: {
|
|
489
|
-
content_type: { type: "string", enum: ["vision", "audio"], description: "
|
|
490
|
-
id: { type: "integer", description: "Content item ID" },
|
|
491
|
-
tags: { type: "array", items: { type: "string" }, description: "Tags to add" },
|
|
502
|
+
content_type: { type: "string", enum: ["vision", "audio"], description: "vision = screen frame, audio = audio chunk. Get the id from search-content results (frame_id / chunk_id)." },
|
|
503
|
+
id: { type: "integer", description: "Content item ID (OCR result frame_id, or audio result chunk_id)" },
|
|
504
|
+
tags: { type: "array", items: { type: "string" }, description: "Tags to add. Prefer namespaced: person:<name>, project:<name>, topic:<name>." },
|
|
492
505
|
},
|
|
493
506
|
required: ["content_type", "id", "tags"],
|
|
494
507
|
},
|
|
@@ -1219,119 +1232,51 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
1219
1232
|
case "export-video": {
|
|
1220
1233
|
const startTime = normalizeTime(args.start_time);
|
|
1221
1234
|
const endTime = normalizeTime(args.end_time);
|
|
1222
|
-
const fps = args.fps || 1.0;
|
|
1223
1235
|
if (!startTime || !endTime) {
|
|
1224
1236
|
return {
|
|
1225
1237
|
content: [{ type: "text", text: "Error: start_time and end_time are required" }],
|
|
1226
1238
|
};
|
|
1227
1239
|
}
|
|
1228
|
-
//
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1240
|
+
// A real-time MP4 with synced microphone audio, rendered server-side by the
|
|
1241
|
+
// engine export core (the `screenpipe export` CLI's HTTP twin). MCP runs on the
|
|
1242
|
+
// same host as the backend, so the returned path is a local file. Frames sit at
|
|
1243
|
+
// their real timestamps, so the clip duration matches the wall-clock span.
|
|
1244
|
+
try {
|
|
1245
|
+
const body = { start: startTime, end: endTime };
|
|
1246
|
+
if (typeof args.output_path === "string" && args.output_path.trim()) {
|
|
1247
|
+
body.output_path = args.output_path;
|
|
1248
|
+
}
|
|
1249
|
+
const response = await callAPI("/export", {
|
|
1250
|
+
method: "POST",
|
|
1251
|
+
body: JSON.stringify(body),
|
|
1252
|
+
});
|
|
1253
|
+
const data = (await response.json());
|
|
1254
|
+
const sizeMb = data.file_size_bytes
|
|
1255
|
+
? (data.file_size_bytes / (1024 * 1024)).toFixed(1)
|
|
1256
|
+
: null;
|
|
1239
1257
|
return {
|
|
1240
1258
|
content: [
|
|
1241
1259
|
{
|
|
1242
1260
|
type: "text",
|
|
1243
|
-
text: `
|
|
1261
|
+
text: `Video exported (with audio): ${data.output_path}\n` +
|
|
1262
|
+
`${data.frame_count ?? 0} frames | ${data.audio_chunk_count ?? 0} audio chunks` +
|
|
1263
|
+
(sizeMb ? ` | ${sizeMb} MB` : "") +
|
|
1264
|
+
(data.duration_secs ? ` | ${data.duration_secs}s` : "") +
|
|
1265
|
+
` | ${startTime} → ${endTime}`,
|
|
1244
1266
|
},
|
|
1245
1267
|
],
|
|
1246
1268
|
};
|
|
1247
1269
|
}
|
|
1248
|
-
|
|
1249
|
-
const seenIds = new Set();
|
|
1250
|
-
for (const result of results) {
|
|
1251
|
-
if (result.type === "OCR" && result.content?.frame_id) {
|
|
1252
|
-
const frameId = result.content.frame_id;
|
|
1253
|
-
if (!seenIds.has(frameId)) {
|
|
1254
|
-
seenIds.add(frameId);
|
|
1255
|
-
frameIds.push(frameId);
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
if (frameIds.length === 0) {
|
|
1260
|
-
return {
|
|
1261
|
-
content: [{ type: "text", text: "No valid frame IDs found (audio-only?)." }],
|
|
1262
|
-
};
|
|
1263
|
-
}
|
|
1264
|
-
frameIds.sort((a, b) => a - b);
|
|
1265
|
-
const wsUrl = `ws://localhost:${port}/frames/export?fps=${fps}`;
|
|
1266
|
-
const exportResult = await new Promise((resolve) => {
|
|
1267
|
-
const ws = new ws_1.WebSocket(wsUrl);
|
|
1268
|
-
let resolved = false;
|
|
1269
|
-
const timeout = setTimeout(() => {
|
|
1270
|
-
if (!resolved) {
|
|
1271
|
-
resolved = true;
|
|
1272
|
-
ws.close();
|
|
1273
|
-
resolve({ success: false, error: "Export timed out after 5 minutes" });
|
|
1274
|
-
}
|
|
1275
|
-
}, 5 * 60 * 1000);
|
|
1276
|
-
ws.on("open", () => {
|
|
1277
|
-
ws.send(JSON.stringify({ frame_ids: frameIds }));
|
|
1278
|
-
});
|
|
1279
|
-
ws.on("error", (error) => {
|
|
1280
|
-
if (!resolved) {
|
|
1281
|
-
resolved = true;
|
|
1282
|
-
clearTimeout(timeout);
|
|
1283
|
-
resolve({ success: false, error: `WebSocket error: ${error.message}` });
|
|
1284
|
-
}
|
|
1285
|
-
});
|
|
1286
|
-
ws.on("close", () => {
|
|
1287
|
-
if (!resolved) {
|
|
1288
|
-
resolved = true;
|
|
1289
|
-
clearTimeout(timeout);
|
|
1290
|
-
resolve({ success: false, error: "Connection closed unexpectedly" });
|
|
1291
|
-
}
|
|
1292
|
-
});
|
|
1293
|
-
ws.on("message", (data) => {
|
|
1294
|
-
try {
|
|
1295
|
-
const message = JSON.parse(data.toString());
|
|
1296
|
-
if (message.status === "completed" && message.video_data) {
|
|
1297
|
-
const tempDir = os.tmpdir();
|
|
1298
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1299
|
-
const filename = `screenpipe_export_${timestamp}.mp4`;
|
|
1300
|
-
const filePath = path.join(tempDir, filename);
|
|
1301
|
-
fs.writeFileSync(filePath, Buffer.from(message.video_data));
|
|
1302
|
-
resolved = true;
|
|
1303
|
-
clearTimeout(timeout);
|
|
1304
|
-
ws.close();
|
|
1305
|
-
resolve({ success: true, filePath, frameCount: frameIds.length });
|
|
1306
|
-
}
|
|
1307
|
-
else if (message.status === "error") {
|
|
1308
|
-
resolved = true;
|
|
1309
|
-
clearTimeout(timeout);
|
|
1310
|
-
ws.close();
|
|
1311
|
-
resolve({ success: false, error: message.error || "Export failed" });
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
catch {
|
|
1315
|
-
// Ignore parse errors for progress messages
|
|
1316
|
-
}
|
|
1317
|
-
});
|
|
1318
|
-
});
|
|
1319
|
-
if (exportResult.success && exportResult.filePath) {
|
|
1270
|
+
catch (err) {
|
|
1320
1271
|
return {
|
|
1321
1272
|
content: [
|
|
1322
1273
|
{
|
|
1323
1274
|
type: "text",
|
|
1324
|
-
text: `
|
|
1325
|
-
`Frames: ${exportResult.frameCount} | ${startTime} → ${endTime} | ${fps} fps`,
|
|
1275
|
+
text: `Export failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1326
1276
|
},
|
|
1327
1277
|
],
|
|
1328
1278
|
};
|
|
1329
1279
|
}
|
|
1330
|
-
else {
|
|
1331
|
-
return {
|
|
1332
|
-
content: [{ type: "text", text: `Export failed: ${exportResult.error}` }],
|
|
1333
|
-
};
|
|
1334
|
-
}
|
|
1335
1280
|
}
|
|
1336
1281
|
case "update-memory": {
|
|
1337
1282
|
if (args.delete && args.id) {
|
package/manifest.json
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
"name": "export-video",
|
|
35
|
-
"description": "Export screen recordings as MP4
|
|
35
|
+
"description": "Export screen recordings as an MP4 for a time range — with synced audio by default (pass fps for a silent timelapse)"
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
"name": "list-meetings",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "screenpipe-mcp",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.11",
|
|
4
4
|
"description": "MCP server for screenpipe - search your screen recordings and audio transcriptions",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -28,14 +28,12 @@
|
|
|
28
28
|
"audio-transcription"
|
|
29
29
|
],
|
|
30
30
|
"author": "Screenpipe",
|
|
31
|
-
"license": "
|
|
31
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@modelcontextprotocol/sdk": "^1.27.1"
|
|
34
|
-
"ws": "^8.19.0"
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.27.1"
|
|
35
34
|
},
|
|
36
35
|
"devDependencies": {
|
|
37
36
|
"@types/node": "^25.3.5",
|
|
38
|
-
"@types/ws": "^8.18.1",
|
|
39
37
|
"typescript": "^5.9.3",
|
|
40
38
|
"ts-node": "^10.9.2",
|
|
41
39
|
"vitest": "^4.0.18"
|