screenpipe-mcp 0.3.1 → 0.4.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/index.js CHANGED
@@ -1,9 +1,59 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
3
36
  Object.defineProperty(exports, "__esModule", { value: true });
4
37
  const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
38
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
39
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
40
+ const ws_1 = require("ws");
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const os = __importStar(require("os"));
44
+ // Helper to get current date in ISO format
45
+ function getCurrentDateInfo() {
46
+ const now = new Date();
47
+ return {
48
+ isoDate: now.toISOString(),
49
+ localDate: now.toLocaleDateString("en-US", {
50
+ weekday: "long",
51
+ year: "numeric",
52
+ month: "long",
53
+ day: "numeric",
54
+ }),
55
+ };
56
+ }
7
57
  // Detect OS
8
58
  const CURRENT_OS = process.platform;
9
59
  const IS_MACOS = CURRENT_OS === "darwin";
@@ -21,60 +71,62 @@ const SCREENPIPE_API = `http://localhost:${port}`;
21
71
  // Initialize server
22
72
  const server = new index_js_1.Server({
23
73
  name: "screenpipe",
24
- version: "0.3.1",
74
+ version: "0.4.0",
25
75
  }, {
26
76
  capabilities: {
27
77
  tools: {},
78
+ prompts: {},
79
+ resources: {},
28
80
  },
29
81
  });
30
82
  // Tool definitions
31
83
  const BASE_TOOLS = [
32
84
  {
33
85
  name: "search-content",
34
- description: "Search through screenpipe recorded content (OCR text, audio transcriptions, UI elements). " +
35
- "Use this to find specific content that has appeared on your screen or been spoken. " +
36
- "Results include timestamps, app context, and the content itself. " +
37
- "Set include_frames=true to get screenshot images for visual analysis (OCR results only).",
86
+ description: "Search screenpipe's recorded content: screen text (OCR), audio transcriptions, and UI elements. " +
87
+ "Returns timestamped results with app context. " +
88
+ "Call with no parameters to get recent activity. " +
89
+ "Use the 'screenpipe://context' resource for current time when building time-based queries.",
38
90
  inputSchema: {
39
91
  type: "object",
40
92
  properties: {
41
93
  q: {
42
94
  type: "string",
43
- description: "Search query to find in recorded content",
95
+ description: "Search query. Optional - omit to return all recent content.",
44
96
  },
45
97
  content_type: {
46
98
  type: "string",
47
99
  enum: ["all", "ocr", "audio", "ui"],
48
- description: "Type of content to search: 'ocr' for screen text, 'audio' for spoken words, 'ui' for UI elements, or 'all' for everything",
100
+ description: "Content type filter. Default: 'all'",
49
101
  default: "all",
50
102
  },
51
103
  limit: {
52
104
  type: "integer",
53
- description: "Maximum number of results to return",
105
+ description: "Max results. Default: 10",
54
106
  default: 10,
55
107
  },
56
108
  offset: {
57
109
  type: "integer",
58
- description: "Number of results to skip (for pagination)",
110
+ description: "Skip N results for pagination. Default: 0",
59
111
  default: 0,
60
112
  },
61
113
  start_time: {
62
114
  type: "string",
63
115
  format: "date-time",
64
- description: "Start time in ISO format UTC (e.g. 2024-01-01T00:00:00Z). Filter results from this time onward.",
116
+ description: "ISO 8601 UTC start time (e.g., 2024-01-15T10:00:00Z)",
65
117
  },
66
118
  end_time: {
67
119
  type: "string",
68
120
  format: "date-time",
69
- description: "End time in ISO format UTC (e.g. 2024-01-01T00:00:00Z). Filter results up to this time.",
121
+ description: "ISO 8601 UTC end time (e.g., 2024-01-15T18:00:00Z)",
70
122
  },
71
123
  app_name: {
72
124
  type: "string",
73
- description: "Filter by application name (e.g. 'Chrome', 'Safari', 'Terminal')",
125
+ description: "Filter by app (e.g., 'Google Chrome', 'Slack', 'zoom.us')",
74
126
  },
75
127
  window_name: {
76
128
  type: "string",
77
- description: "Filter by window name or title",
129
+ description: "Filter by window title",
78
130
  },
79
131
  min_length: {
80
132
  type: "integer",
@@ -86,9 +138,7 @@ const BASE_TOOLS = [
86
138
  },
87
139
  include_frames: {
88
140
  type: "boolean",
89
- description: "Include screenshot images in results for visual analysis. Only applies to OCR results. " +
90
- "When true, returns base64-encoded images that can be analyzed with vision capabilities. " +
91
- "Note: Images are limited to ~1MB each. Default: false",
141
+ description: "Include base64 screenshots (OCR only). Default: false",
92
142
  default: false,
93
143
  },
94
144
  },
@@ -138,6 +188,36 @@ const BASE_TOOLS = [
138
188
  required: ["action_type", "data"],
139
189
  },
140
190
  },
191
+ {
192
+ name: "export-video",
193
+ description: "Export a video of screen recordings for a specific time range. " +
194
+ "Creates an MP4 video from the recorded frames between the start and end times.\n\n" +
195
+ "IMPORTANT: Use ISO 8601 UTC timestamps (e.g., 2024-01-15T10:00:00Z)\n\n" +
196
+ "EXAMPLES:\n" +
197
+ "- Last 30 minutes: Calculate timestamps from current time\n" +
198
+ "- Specific meeting: Use the meeting's start and end times in UTC",
199
+ inputSchema: {
200
+ type: "object",
201
+ properties: {
202
+ start_time: {
203
+ type: "string",
204
+ format: "date-time",
205
+ description: "Start time in ISO 8601 format UTC. MUST include timezone (Z for UTC). Example: '2024-01-15T10:00:00Z'",
206
+ },
207
+ end_time: {
208
+ type: "string",
209
+ format: "date-time",
210
+ description: "End time in ISO 8601 format UTC. MUST include timezone (Z for UTC). Example: '2024-01-15T10:30:00Z'",
211
+ },
212
+ fps: {
213
+ type: "number",
214
+ description: "Frames per second for the output video. Lower values (0.5-1.0) create smaller files, higher values (5-10) create smoother playback. Default: 1.0",
215
+ default: 1.0,
216
+ },
217
+ },
218
+ required: ["start_time", "end_time"],
219
+ },
220
+ },
141
221
  ];
142
222
  const MACOS_TOOLS = [
143
223
  {
@@ -339,6 +419,209 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
339
419
  }
340
420
  return { tools };
341
421
  });
422
+ // MCP Resources - provide dynamic context data
423
+ const RESOURCES = [
424
+ {
425
+ uri: "screenpipe://context",
426
+ name: "Current Context",
427
+ description: "Current date/time and pre-computed timestamps for common time ranges",
428
+ mimeType: "application/json",
429
+ },
430
+ {
431
+ uri: "screenpipe://guide",
432
+ name: "Usage Guide",
433
+ description: "How to use screenpipe search effectively",
434
+ mimeType: "text/markdown",
435
+ },
436
+ ];
437
+ // List resources handler
438
+ server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
439
+ return { resources: RESOURCES };
440
+ });
441
+ // Read resource handler
442
+ server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) => {
443
+ const { uri } = request.params;
444
+ const dateInfo = getCurrentDateInfo();
445
+ const now = Date.now();
446
+ switch (uri) {
447
+ case "screenpipe://context":
448
+ return {
449
+ contents: [
450
+ {
451
+ uri,
452
+ mimeType: "application/json",
453
+ text: JSON.stringify({
454
+ current_time: dateInfo.isoDate,
455
+ current_date_local: dateInfo.localDate,
456
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
457
+ timestamps: {
458
+ now: dateInfo.isoDate,
459
+ one_hour_ago: new Date(now - 60 * 60 * 1000).toISOString(),
460
+ three_hours_ago: new Date(now - 3 * 60 * 60 * 1000).toISOString(),
461
+ today_start: `${new Date().toISOString().split("T")[0]}T00:00:00Z`,
462
+ yesterday_start: `${new Date(now - 24 * 60 * 60 * 1000).toISOString().split("T")[0]}T00:00:00Z`,
463
+ one_week_ago: new Date(now - 7 * 24 * 60 * 60 * 1000).toISOString(),
464
+ },
465
+ common_apps: ["Google Chrome", "Safari", "Slack", "zoom.us", "Microsoft Teams", "Code", "Terminal"],
466
+ }, null, 2),
467
+ },
468
+ ],
469
+ };
470
+ case "screenpipe://guide":
471
+ return {
472
+ contents: [
473
+ {
474
+ uri,
475
+ mimeType: "text/markdown",
476
+ text: `# Screenpipe Search Guide
477
+
478
+ ## Quick Start
479
+ - **Get recent activity**: Call search-content with no parameters
480
+ - **Search text**: \`{"q": "search term", "content_type": "ocr"}\`
481
+ - **Time filter**: Use start_time/end_time with ISO 8601 UTC timestamps
482
+
483
+ ## Content Types
484
+ - \`ocr\`: Screen text (what you see)
485
+ - \`audio\`: Transcribed speech
486
+ - \`ui\`: UI element interactions
487
+ - \`all\`: Everything (default)
488
+
489
+ ## Key Parameters
490
+ | Parameter | Description | Default |
491
+ |-----------|-------------|---------|
492
+ | q | Search query | (none - returns all) |
493
+ | content_type | ocr/audio/ui/all | all |
494
+ | limit | Max results | 10 |
495
+ | start_time | ISO 8601 UTC | (no filter) |
496
+ | end_time | ISO 8601 UTC | (no filter) |
497
+ | app_name | Filter by app | (no filter) |
498
+ | include_frames | Include screenshots | false |
499
+
500
+ ## Tips
501
+ 1. Read screenpipe://context first to get current timestamps
502
+ 2. Omit \`q\` to get all content (useful for "what was I doing?")
503
+ 3. Use \`limit: 50-100\` for comprehensive searches
504
+ 4. Combine app_name + time filters for focused results`,
505
+ },
506
+ ],
507
+ };
508
+ default:
509
+ throw new Error(`Unknown resource: ${uri}`);
510
+ }
511
+ });
512
+ // MCP Prompts - static interaction templates
513
+ const PROMPTS = [
514
+ {
515
+ name: "search-recent",
516
+ description: "Search recent screen activity",
517
+ arguments: [
518
+ { name: "query", description: "Optional search term", required: false },
519
+ { name: "hours", description: "Hours to look back (default: 1)", required: false },
520
+ ],
521
+ },
522
+ {
523
+ name: "find-in-app",
524
+ description: "Find content from a specific application",
525
+ arguments: [
526
+ { name: "app", description: "App name (e.g., Chrome, Slack)", required: true },
527
+ { name: "query", description: "Optional search term", required: false },
528
+ ],
529
+ },
530
+ {
531
+ name: "meeting-notes",
532
+ description: "Get audio transcriptions from meetings",
533
+ arguments: [
534
+ { name: "hours", description: "Hours to look back (default: 3)", required: false },
535
+ ],
536
+ },
537
+ ];
538
+ // List prompts handler
539
+ server.setRequestHandler(types_js_1.ListPromptsRequestSchema, async () => {
540
+ return { prompts: PROMPTS };
541
+ });
542
+ // Get prompt handler
543
+ server.setRequestHandler(types_js_1.GetPromptRequestSchema, async (request) => {
544
+ const { name, arguments: promptArgs } = request.params;
545
+ const dateInfo = getCurrentDateInfo();
546
+ const now = Date.now();
547
+ switch (name) {
548
+ case "search-recent": {
549
+ const query = promptArgs?.query || "";
550
+ const hours = parseInt(promptArgs?.hours || "1", 10);
551
+ const startTime = new Date(now - hours * 60 * 60 * 1000).toISOString();
552
+ return {
553
+ description: `Search recent activity (last ${hours} hour${hours > 1 ? "s" : ""})`,
554
+ messages: [
555
+ {
556
+ role: "user",
557
+ content: {
558
+ type: "text",
559
+ text: `Search screenpipe for recent activity.
560
+
561
+ Current time: ${dateInfo.isoDate}
562
+
563
+ Use search-content with:
564
+ ${query ? `- q: "${query}"` : "- No query filter (get all content)"}
565
+ - start_time: "${startTime}"
566
+ - limit: 50`,
567
+ },
568
+ },
569
+ ],
570
+ };
571
+ }
572
+ case "find-in-app": {
573
+ const app = promptArgs?.app || "Google Chrome";
574
+ const query = promptArgs?.query || "";
575
+ return {
576
+ description: `Find content from ${app}`,
577
+ messages: [
578
+ {
579
+ role: "user",
580
+ content: {
581
+ type: "text",
582
+ text: `Search screenpipe for content from ${app}.
583
+
584
+ Current time: ${dateInfo.isoDate}
585
+
586
+ Use search-content with:
587
+ - app_name: "${app}"
588
+ ${query ? `- q: "${query}"` : "- No query filter"}
589
+ - content_type: "ocr"
590
+ - limit: 50`,
591
+ },
592
+ },
593
+ ],
594
+ };
595
+ }
596
+ case "meeting-notes": {
597
+ const hours = parseInt(promptArgs?.hours || "3", 10);
598
+ const startTime = new Date(now - hours * 60 * 60 * 1000).toISOString();
599
+ return {
600
+ description: `Get meeting transcriptions (last ${hours} hours)`,
601
+ messages: [
602
+ {
603
+ role: "user",
604
+ content: {
605
+ type: "text",
606
+ text: `Get audio transcriptions from recent meetings.
607
+
608
+ Current time: ${dateInfo.isoDate}
609
+
610
+ Use search-content with:
611
+ - content_type: "audio"
612
+ - start_time: "${startTime}"
613
+ - limit: 100
614
+
615
+ Common meeting apps: zoom.us, Microsoft Teams, Google Meet, Slack`,
616
+ },
617
+ },
618
+ ],
619
+ };
620
+ }
621
+ default:
622
+ throw new Error(`Unknown prompt: ${name}`);
623
+ }
624
+ });
342
625
  // Helper function to make HTTP requests
343
626
  async function fetchAPI(endpoint, options = {}) {
344
627
  const url = `${SCREENPIPE_API}${endpoint}`;
@@ -391,9 +674,15 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
391
674
  }
392
675
  const data = await response.json();
393
676
  const results = data.data || [];
677
+ const pagination = data.pagination || {};
394
678
  if (results.length === 0) {
395
679
  return {
396
- content: [{ type: "text", text: "No results found" }],
680
+ content: [
681
+ {
682
+ type: "text",
683
+ text: "No results found. Try: broader search terms, different content_type, or wider time range.",
684
+ },
685
+ ],
397
686
  };
398
687
  }
399
688
  // Build content array with text and optional images
@@ -405,57 +694,38 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
405
694
  if (!content)
406
695
  continue;
407
696
  if (result.type === "OCR") {
408
- const textResult = `OCR Text: ${content.text || "N/A"}\n` +
409
- `App: ${content.app_name || "N/A"}\n` +
410
- `Window: ${content.window_name || "N/A"}\n` +
411
- `Time: ${content.timestamp || "N/A"}\n` +
412
- `Frame ID: ${content.frame_id || "N/A"}\n` +
413
- "---";
414
- formattedResults.push(textResult);
415
- // Collect frame if available and requested
697
+ formattedResults.push(`[OCR] ${content.app_name || "?"} | ${content.window_name || "?"}\n` +
698
+ `${content.timestamp || ""}\n` +
699
+ `${content.text || ""}`);
416
700
  if (includeFrames && content.frame) {
417
701
  images.push({
418
702
  data: content.frame,
419
- context: `Screenshot from ${content.app_name || "unknown"} - ${content.window_name || "unknown"} at ${content.timestamp || "unknown"}`,
703
+ context: `${content.app_name} at ${content.timestamp}`,
420
704
  });
421
705
  }
422
706
  }
423
707
  else if (result.type === "Audio") {
424
- formattedResults.push(`Audio Transcription: ${content.transcription || "N/A"}\n` +
425
- `Device: ${content.device_name || "N/A"}\n` +
426
- `Time: ${content.timestamp || "N/A"}\n` +
427
- "---");
708
+ formattedResults.push(`[Audio] ${content.device_name || "?"}\n` +
709
+ `${content.timestamp || ""}\n` +
710
+ `${content.transcription || ""}`);
428
711
  }
429
712
  else if (result.type === "UI") {
430
- formattedResults.push(`UI Text: ${content.text || "N/A"}\n` +
431
- `App: ${content.app_name || "N/A"}\n` +
432
- `Window: ${content.window_name || "N/A"}\n` +
433
- `Time: ${content.timestamp || "N/A"}\n` +
434
- "---");
713
+ formattedResults.push(`[UI] ${content.app_name || "?"} | ${content.window_name || "?"}\n` +
714
+ `${content.timestamp || ""}\n` +
715
+ `${content.text || ""}`);
435
716
  }
436
717
  }
437
- // Add text results
718
+ // Header with pagination info
719
+ const header = `Results: ${results.length}/${pagination.total || "?"}` +
720
+ (pagination.total > results.length ? ` (use offset=${(pagination.offset || 0) + results.length} for more)` : "");
438
721
  contentItems.push({
439
722
  type: "text",
440
- text: "Search Results:\n\n" +
441
- formattedResults.join("\n") +
442
- (images.length > 0
443
- ? `\n\n${images.length} screenshot(s) included below for visual analysis:`
444
- : ""),
723
+ text: header + "\n\n" + formattedResults.join("\n---\n"),
445
724
  });
446
- // Add images if requested and available
725
+ // Add images if requested
447
726
  for (const img of images) {
448
- // Add context for the image
449
- contentItems.push({
450
- type: "text",
451
- text: `\n📷 ${img.context}`,
452
- });
453
- // Add the image itself
454
- contentItems.push({
455
- type: "image",
456
- data: img.data,
457
- mimeType: "image/png",
458
- });
727
+ contentItems.push({ type: "text", text: `\n📷 ${img.context}` });
728
+ contentItems.push({ type: "image", data: img.data, mimeType: "image/png" });
459
729
  }
460
730
  return { content: contentItems };
461
731
  }
@@ -500,6 +770,151 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
500
770
  content: [{ type: "text", text: resultText }],
501
771
  };
502
772
  }
773
+ case "export-video": {
774
+ const startTime = args.start_time;
775
+ const endTime = args.end_time;
776
+ const fps = args.fps || 1.0;
777
+ // Validate time inputs
778
+ if (!startTime || !endTime) {
779
+ return {
780
+ content: [
781
+ {
782
+ type: "text",
783
+ text: "Error: Both start_time and end_time are required in ISO 8601 format (e.g., '2024-01-15T10:00:00Z')",
784
+ },
785
+ ],
786
+ };
787
+ }
788
+ // Step 1: Query the search API to get frame IDs for the time range
789
+ const searchParams = new URLSearchParams({
790
+ content_type: "ocr",
791
+ start_time: startTime,
792
+ end_time: endTime,
793
+ limit: "10000", // Get all frames in range
794
+ });
795
+ const searchResponse = await fetchAPI(`/search?${searchParams.toString()}`);
796
+ if (!searchResponse.ok) {
797
+ throw new Error(`Failed to search for frames: HTTP ${searchResponse.status}`);
798
+ }
799
+ const searchData = await searchResponse.json();
800
+ const results = searchData.data || [];
801
+ if (results.length === 0) {
802
+ return {
803
+ content: [
804
+ {
805
+ type: "text",
806
+ text: `No screen recordings found between ${startTime} and ${endTime}. Make sure screenpipe was recording during this time period.`,
807
+ },
808
+ ],
809
+ };
810
+ }
811
+ // Extract unique frame IDs from OCR results
812
+ const frameIds = [];
813
+ const seenIds = new Set();
814
+ for (const result of results) {
815
+ if (result.type === "OCR" && result.content?.frame_id) {
816
+ const frameId = result.content.frame_id;
817
+ if (!seenIds.has(frameId)) {
818
+ seenIds.add(frameId);
819
+ frameIds.push(frameId);
820
+ }
821
+ }
822
+ }
823
+ if (frameIds.length === 0) {
824
+ return {
825
+ content: [
826
+ {
827
+ type: "text",
828
+ text: `Found ${results.length} results but no valid frame IDs. The recordings may be audio-only.`,
829
+ },
830
+ ],
831
+ };
832
+ }
833
+ // Sort frame IDs
834
+ frameIds.sort((a, b) => a - b);
835
+ // Step 2: Connect to WebSocket and export video
836
+ const wsUrl = `ws://localhost:${port}/frames/export?frame_ids=${frameIds.join(",")}&fps=${fps}`;
837
+ const exportResult = await new Promise((resolve) => {
838
+ const ws = new ws_1.WebSocket(wsUrl);
839
+ let resolved = false;
840
+ const timeout = setTimeout(() => {
841
+ if (!resolved) {
842
+ resolved = true;
843
+ ws.close();
844
+ resolve({ success: false, error: "Export timed out after 5 minutes" });
845
+ }
846
+ }, 5 * 60 * 1000); // 5 minute timeout
847
+ ws.on("error", (error) => {
848
+ if (!resolved) {
849
+ resolved = true;
850
+ clearTimeout(timeout);
851
+ resolve({ success: false, error: `WebSocket error: ${error.message}` });
852
+ }
853
+ });
854
+ ws.on("close", () => {
855
+ if (!resolved) {
856
+ resolved = true;
857
+ clearTimeout(timeout);
858
+ resolve({ success: false, error: "Connection closed unexpectedly" });
859
+ }
860
+ });
861
+ ws.on("message", (data) => {
862
+ try {
863
+ const message = JSON.parse(data.toString());
864
+ if (message.status === "completed" && message.video_data) {
865
+ // Save video to temp file
866
+ const tempDir = os.tmpdir();
867
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
868
+ const filename = `screenpipe_export_${timestamp}.mp4`;
869
+ const filePath = path.join(tempDir, filename);
870
+ fs.writeFileSync(filePath, Buffer.from(message.video_data));
871
+ resolved = true;
872
+ clearTimeout(timeout);
873
+ ws.close();
874
+ resolve({
875
+ success: true,
876
+ filePath,
877
+ frameCount: frameIds.length,
878
+ });
879
+ }
880
+ else if (message.status === "error") {
881
+ resolved = true;
882
+ clearTimeout(timeout);
883
+ ws.close();
884
+ resolve({ success: false, error: message.error || "Export failed" });
885
+ }
886
+ // Ignore "extracting" and "encoding" status updates
887
+ }
888
+ catch (parseError) {
889
+ // Ignore parse errors for progress messages
890
+ }
891
+ });
892
+ });
893
+ if (exportResult.success && exportResult.filePath) {
894
+ return {
895
+ content: [
896
+ {
897
+ type: "text",
898
+ text: `Successfully exported video!\n\n` +
899
+ `File: ${exportResult.filePath}\n` +
900
+ `Frames: ${exportResult.frameCount}\n` +
901
+ `Time range: ${startTime} to ${endTime}\n` +
902
+ `FPS: ${fps}`,
903
+ },
904
+ ],
905
+ };
906
+ }
907
+ else {
908
+ return {
909
+ content: [
910
+ {
911
+ type: "text",
912
+ text: `Failed to export video: ${exportResult.error}`,
913
+ },
914
+ ],
915
+ };
916
+ }
917
+ }
503
918
  case "click-element": {
504
919
  const selector = {
505
920
  app_name: args.app,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screenpipe-mcp",
3
- "version": "0.3.1",
3
+ "version": "0.4.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": {
@@ -10,6 +10,8 @@
10
10
  "build": "tsc",
11
11
  "start": "node dist/index.js",
12
12
  "dev": "ts-node src/index.ts",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
13
15
  "prepublishOnly": "npm run build"
14
16
  },
15
17
  "keywords": [
@@ -25,12 +27,15 @@
25
27
  "author": "Mediar AI",
26
28
  "license": "MIT",
27
29
  "dependencies": {
28
- "@modelcontextprotocol/sdk": "^1.0.0"
30
+ "@modelcontextprotocol/sdk": "^1.0.0",
31
+ "ws": "^8.18.0"
29
32
  },
30
33
  "devDependencies": {
31
34
  "@types/node": "^20.0.0",
35
+ "@types/ws": "^8.5.13",
32
36
  "typescript": "^5.0.0",
33
- "ts-node": "^10.9.0"
37
+ "ts-node": "^10.9.0",
38
+ "vitest": "^2.1.0"
34
39
  },
35
40
  "engines": {
36
41
  "node": ">=18.0.0"