screenpipe-mcp 0.18.8 → 0.18.10
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/bun.lock +0 -10
- package/dist/export-video.test.js +79 -271
- package/dist/index.js +74 -110
- package/manifest.json +1 -1
- package/package.json +2 -4
- package/src/export-video.test.ts +89 -317
- package/src/index.ts +84 -126
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"));
|
|
@@ -397,15 +396,21 @@ const TOOLS = [
|
|
|
397
396
|
},
|
|
398
397
|
{
|
|
399
398
|
name: "export-video",
|
|
400
|
-
description: "Export an MP4
|
|
401
|
-
"
|
|
399
|
+
description: "Export an MP4 of screen recordings for a time range, with synced microphone audio. " +
|
|
400
|
+
"Frames are placed at their real timestamps, so the clip's duration matches the " +
|
|
401
|
+
"wall-clock span you requested (not a sped-up timelapse). Returns the file path. " +
|
|
402
|
+
"Can take a few minutes for long ranges.",
|
|
402
403
|
annotations: { title: "Export Video", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
403
404
|
inputSchema: {
|
|
404
405
|
type: "object",
|
|
405
406
|
properties: {
|
|
406
|
-
start_time: { type: "string", description:
|
|
407
|
-
end_time: { type: "string", description:
|
|
408
|
-
|
|
407
|
+
start_time: { type: "string", description: 'ISO 8601 UTC or relative (e.g. "5m ago", "now")' },
|
|
408
|
+
end_time: { type: "string", description: 'ISO 8601 UTC or relative (e.g. "5m ago", "now")' },
|
|
409
|
+
output_path: {
|
|
410
|
+
type: "string",
|
|
411
|
+
description: "Optional absolute path for the MP4 (e.g. ~/Downloads/clip.mp4). " +
|
|
412
|
+
"Defaults to the screenpipe data dir's exports/ folder.",
|
|
413
|
+
},
|
|
409
414
|
},
|
|
410
415
|
required: ["start_time", "end_time"],
|
|
411
416
|
},
|
|
@@ -685,24 +690,46 @@ const TEAM_TOOLS = [
|
|
|
685
690
|
},
|
|
686
691
|
{
|
|
687
692
|
name: "team-records",
|
|
688
|
-
description: "Chronological
|
|
689
|
-
"
|
|
690
|
-
"
|
|
693
|
+
description: "Chronological dump of the org's data for a time window — both raw " +
|
|
694
|
+
"telemetry (frame/audio) and the structured outputs of the enterprise-" +
|
|
695
|
+
"worker pipes (sop/skill/trajectory/memory/workflow). " +
|
|
696
|
+
"Raw kinds return oldest → newest (vs team-search which is recency-ranked). " +
|
|
697
|
+
"Synthesized kinds return one record per device's latest run by default " +
|
|
698
|
+
"(set latest_only=false to walk run history). " +
|
|
699
|
+
"Use raw for ETL / \"walk me through X from Y to Z\". " +
|
|
700
|
+
"Use synthesized for \"what SOPs / skills / trajectories / memories did " +
|
|
701
|
+
"we extract from my team's work\" — each item carries evidence-cited " +
|
|
702
|
+
"event_ids/frame_ids that team-search can resolve back to raw records. " +
|
|
691
703
|
"Auth: enterprise admin token.",
|
|
692
704
|
annotations: { title: "Team Records", readOnlyHint: true, openWorldHint: true, idempotentHint: true },
|
|
693
705
|
inputSchema: {
|
|
694
706
|
type: "object",
|
|
695
707
|
properties: {
|
|
696
|
-
device_id: { type: "string", description: "Restrict to one device (optional)." },
|
|
697
|
-
kind: {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
708
|
+
device_id: { type: "string", description: "Restrict to one device (optional). Raw kinds only." },
|
|
709
|
+
kind: {
|
|
710
|
+
type: "string",
|
|
711
|
+
enum: ["frame", "audio", "all", "sop", "skill", "trajectory", "memory", "workflow"],
|
|
712
|
+
description: "What to return. Raw: frame|audio|all (telemetry). " +
|
|
713
|
+
"Synthesized: sop|skill|trajectory|memory|workflow (pipe outputs). " +
|
|
714
|
+
"Default: all.",
|
|
715
|
+
default: "all",
|
|
716
|
+
},
|
|
717
|
+
since: { type: "string", description: "ISO 8601 lower bound. Raw kinds only." },
|
|
718
|
+
until: { type: "string", description: "ISO 8601 upper bound. Raw kinds only." },
|
|
719
|
+
since_hours_ago: { type: "integer", description: "Convenience: equivalent to since=now-N*h. Raw kinds only." },
|
|
720
|
+
limit: { type: "integer", description: "Max records (default 50, max 200). Raw kinds only.", default: 50 },
|
|
721
|
+
latest_only: {
|
|
722
|
+
type: "boolean",
|
|
723
|
+
description: "Synthesized kinds only: if true (default), collapse to the newest " +
|
|
724
|
+
"run per device. Set false to walk run history.",
|
|
725
|
+
default: true,
|
|
726
|
+
},
|
|
702
727
|
},
|
|
703
728
|
},
|
|
704
729
|
},
|
|
705
730
|
];
|
|
731
|
+
// Pipe-output kinds map to /workflows/generated, raw kinds map to /records.
|
|
732
|
+
const SYNTHESIZED_KINDS = new Set(["sop", "skill", "trajectory", "memory", "workflow"]);
|
|
706
733
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
707
734
|
// Team tools only surface when an enterprise token was discovered at boot.
|
|
708
735
|
// No token = consumer / non-admin user; their MCP looks identical to today.
|
|
@@ -1197,119 +1224,51 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
1197
1224
|
case "export-video": {
|
|
1198
1225
|
const startTime = normalizeTime(args.start_time);
|
|
1199
1226
|
const endTime = normalizeTime(args.end_time);
|
|
1200
|
-
const fps = args.fps || 1.0;
|
|
1201
1227
|
if (!startTime || !endTime) {
|
|
1202
1228
|
return {
|
|
1203
1229
|
content: [{ type: "text", text: "Error: start_time and end_time are required" }],
|
|
1204
1230
|
};
|
|
1205
1231
|
}
|
|
1206
|
-
//
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1232
|
+
// A real-time MP4 with synced microphone audio, rendered server-side by the
|
|
1233
|
+
// engine export core (the `screenpipe export` CLI's HTTP twin). MCP runs on the
|
|
1234
|
+
// same host as the backend, so the returned path is a local file. Frames sit at
|
|
1235
|
+
// their real timestamps, so the clip duration matches the wall-clock span.
|
|
1236
|
+
try {
|
|
1237
|
+
const body = { start: startTime, end: endTime };
|
|
1238
|
+
if (typeof args.output_path === "string" && args.output_path.trim()) {
|
|
1239
|
+
body.output_path = args.output_path;
|
|
1240
|
+
}
|
|
1241
|
+
const response = await callAPI("/export", {
|
|
1242
|
+
method: "POST",
|
|
1243
|
+
body: JSON.stringify(body),
|
|
1244
|
+
});
|
|
1245
|
+
const data = (await response.json());
|
|
1246
|
+
const sizeMb = data.file_size_bytes
|
|
1247
|
+
? (data.file_size_bytes / (1024 * 1024)).toFixed(1)
|
|
1248
|
+
: null;
|
|
1217
1249
|
return {
|
|
1218
1250
|
content: [
|
|
1219
1251
|
{
|
|
1220
1252
|
type: "text",
|
|
1221
|
-
text: `
|
|
1253
|
+
text: `Video exported (with audio): ${data.output_path}\n` +
|
|
1254
|
+
`${data.frame_count ?? 0} frames | ${data.audio_chunk_count ?? 0} audio chunks` +
|
|
1255
|
+
(sizeMb ? ` | ${sizeMb} MB` : "") +
|
|
1256
|
+
(data.duration_secs ? ` | ${data.duration_secs}s` : "") +
|
|
1257
|
+
` | ${startTime} → ${endTime}`,
|
|
1222
1258
|
},
|
|
1223
1259
|
],
|
|
1224
1260
|
};
|
|
1225
1261
|
}
|
|
1226
|
-
|
|
1227
|
-
const seenIds = new Set();
|
|
1228
|
-
for (const result of results) {
|
|
1229
|
-
if (result.type === "OCR" && result.content?.frame_id) {
|
|
1230
|
-
const frameId = result.content.frame_id;
|
|
1231
|
-
if (!seenIds.has(frameId)) {
|
|
1232
|
-
seenIds.add(frameId);
|
|
1233
|
-
frameIds.push(frameId);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
if (frameIds.length === 0) {
|
|
1238
|
-
return {
|
|
1239
|
-
content: [{ type: "text", text: "No valid frame IDs found (audio-only?)." }],
|
|
1240
|
-
};
|
|
1241
|
-
}
|
|
1242
|
-
frameIds.sort((a, b) => a - b);
|
|
1243
|
-
const wsUrl = `ws://localhost:${port}/frames/export?fps=${fps}`;
|
|
1244
|
-
const exportResult = await new Promise((resolve) => {
|
|
1245
|
-
const ws = new ws_1.WebSocket(wsUrl);
|
|
1246
|
-
let resolved = false;
|
|
1247
|
-
const timeout = setTimeout(() => {
|
|
1248
|
-
if (!resolved) {
|
|
1249
|
-
resolved = true;
|
|
1250
|
-
ws.close();
|
|
1251
|
-
resolve({ success: false, error: "Export timed out after 5 minutes" });
|
|
1252
|
-
}
|
|
1253
|
-
}, 5 * 60 * 1000);
|
|
1254
|
-
ws.on("open", () => {
|
|
1255
|
-
ws.send(JSON.stringify({ frame_ids: frameIds }));
|
|
1256
|
-
});
|
|
1257
|
-
ws.on("error", (error) => {
|
|
1258
|
-
if (!resolved) {
|
|
1259
|
-
resolved = true;
|
|
1260
|
-
clearTimeout(timeout);
|
|
1261
|
-
resolve({ success: false, error: `WebSocket error: ${error.message}` });
|
|
1262
|
-
}
|
|
1263
|
-
});
|
|
1264
|
-
ws.on("close", () => {
|
|
1265
|
-
if (!resolved) {
|
|
1266
|
-
resolved = true;
|
|
1267
|
-
clearTimeout(timeout);
|
|
1268
|
-
resolve({ success: false, error: "Connection closed unexpectedly" });
|
|
1269
|
-
}
|
|
1270
|
-
});
|
|
1271
|
-
ws.on("message", (data) => {
|
|
1272
|
-
try {
|
|
1273
|
-
const message = JSON.parse(data.toString());
|
|
1274
|
-
if (message.status === "completed" && message.video_data) {
|
|
1275
|
-
const tempDir = os.tmpdir();
|
|
1276
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1277
|
-
const filename = `screenpipe_export_${timestamp}.mp4`;
|
|
1278
|
-
const filePath = path.join(tempDir, filename);
|
|
1279
|
-
fs.writeFileSync(filePath, Buffer.from(message.video_data));
|
|
1280
|
-
resolved = true;
|
|
1281
|
-
clearTimeout(timeout);
|
|
1282
|
-
ws.close();
|
|
1283
|
-
resolve({ success: true, filePath, frameCount: frameIds.length });
|
|
1284
|
-
}
|
|
1285
|
-
else if (message.status === "error") {
|
|
1286
|
-
resolved = true;
|
|
1287
|
-
clearTimeout(timeout);
|
|
1288
|
-
ws.close();
|
|
1289
|
-
resolve({ success: false, error: message.error || "Export failed" });
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
catch {
|
|
1293
|
-
// Ignore parse errors for progress messages
|
|
1294
|
-
}
|
|
1295
|
-
});
|
|
1296
|
-
});
|
|
1297
|
-
if (exportResult.success && exportResult.filePath) {
|
|
1262
|
+
catch (err) {
|
|
1298
1263
|
return {
|
|
1299
1264
|
content: [
|
|
1300
1265
|
{
|
|
1301
1266
|
type: "text",
|
|
1302
|
-
text: `
|
|
1303
|
-
`Frames: ${exportResult.frameCount} | ${startTime} → ${endTime} | ${fps} fps`,
|
|
1267
|
+
text: `Export failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1304
1268
|
},
|
|
1305
1269
|
],
|
|
1306
1270
|
};
|
|
1307
1271
|
}
|
|
1308
|
-
else {
|
|
1309
|
-
return {
|
|
1310
|
-
content: [{ type: "text", text: `Export failed: ${exportResult.error}` }],
|
|
1311
|
-
};
|
|
1312
|
-
}
|
|
1313
1272
|
}
|
|
1314
1273
|
case "update-memory": {
|
|
1315
1274
|
if (args.delete && args.id) {
|
|
@@ -1556,7 +1515,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
1556
1515
|
};
|
|
1557
1516
|
}
|
|
1558
1517
|
const response = await callAPI(`/meetings/${meetingId}`, {
|
|
1559
|
-
method: "
|
|
1518
|
+
method: "PUT",
|
|
1560
1519
|
headers: { "Content-Type": "application/json" },
|
|
1561
1520
|
body: JSON.stringify(body),
|
|
1562
1521
|
});
|
|
@@ -1681,10 +1640,15 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
1681
1640
|
],
|
|
1682
1641
|
};
|
|
1683
1642
|
}
|
|
1684
|
-
// Map MCP tool name → /api/enterprise/v1 path
|
|
1643
|
+
// Map MCP tool name → /api/enterprise/v1 path. team-records also
|
|
1644
|
+
// routes synthesized pipe outputs (kind=sop|skill|...) to the
|
|
1645
|
+
// workflows endpoint so callers see one tool surface for "give me
|
|
1646
|
+
// the org's data."
|
|
1647
|
+
const kindArg = typeof args.kind === "string" ? args.kind : "";
|
|
1685
1648
|
const subpath = name === "team-search" ? "/search"
|
|
1686
1649
|
: name === "team-devices" ? "/devices"
|
|
1687
|
-
: "
|
|
1650
|
+
: name === "team-records" && SYNTHESIZED_KINDS.has(kindArg) ? "/workflows/generated"
|
|
1651
|
+
: "/records";
|
|
1688
1652
|
// Forward every primitive arg as a query param. The server validates;
|
|
1689
1653
|
// unknown params are ignored, so we don't need to gatekeep here.
|
|
1690
1654
|
const params = new URLSearchParams();
|
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.10",
|
|
4
4
|
"description": "MCP server for screenpipe - search your screen recordings and audio transcriptions",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -30,12 +30,10 @@
|
|
|
30
30
|
"author": "Screenpipe",
|
|
31
31
|
"license": "MIT",
|
|
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"
|