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/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 video of screen recordings for a time range. " +
401
- "Returns the file path. Can take a few minutes for long ranges.",
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: "ISO 8601 UTC or relative" },
407
- end_time: { type: "string", description: "ISO 8601 UTC or relative" },
408
- fps: { type: "number", description: "Output FPS (default 1.0). Higher = smoother but larger file.", default: 1.0 },
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 raw dump of the org's telemetry for a time window. " +
689
- "Returns oldest newest (vs team-search which is recency-ranked). " +
690
- "Use for ETL or \"walk me through X from Y to Z\" — NOT for question-answering, use team-search for that. " +
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: { type: "string", enum: ["frame", "audio", "all"], description: "Record kind filter. Default: all.", default: "all" },
698
- since: { type: "string", description: "ISO 8601 lower bound." },
699
- until: { type: "string", description: "ISO 8601 upper bound." },
700
- since_hours_ago: { type: "integer", description: "Convenience: equivalent to since=now-N*h." },
701
- limit: { type: "integer", description: "Max records (default 50, max 200).", default: 50 },
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
- // Get frame IDs for the time range
1207
- const searchParams = new URLSearchParams({
1208
- content_type: "ocr",
1209
- start_time: startTime,
1210
- end_time: endTime,
1211
- limit: "10000",
1212
- });
1213
- const searchResponse = await callAPI(`/search?${searchParams.toString()}`);
1214
- const searchData = await searchResponse.json();
1215
- const results = searchData.data || [];
1216
- if (results.length === 0) {
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: `No screen recordings found between ${startTime} and ${endTime}.`,
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
- const frameIds = [];
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: `Video exported: ${exportResult.filePath}\n` +
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: "PATCH",
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
- : "/records";
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 video for a specific time range"
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.8",
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"