screenpipe-mcp 0.19.0 → 0.19.4

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.
Files changed (64) hide show
  1. package/README.md +22 -12
  2. package/bun.lock +1 -3
  3. package/dist/cli.js +52886 -44
  4. package/dist/http-server.js +50456 -328
  5. package/dist/index.js +51268 -2003
  6. package/package.json +4 -6
  7. package/scripts/assert-pack-contents.js +60 -28
  8. package/scripts/build.mjs +34 -0
  9. package/server.json +2 -2
  10. package/src/activity-summary-format.test.ts +109 -0
  11. package/src/activity-summary-format.ts +253 -0
  12. package/src/activity-summary-tool.test.ts +133 -0
  13. package/src/activity-summary-tool.ts +79 -0
  14. package/src/api-base.test.ts +39 -0
  15. package/src/api-base.ts +24 -0
  16. package/src/http-server.ts +6 -5
  17. package/src/index.ts +293 -211
  18. package/src/notification-request.test.ts +17 -1
  19. package/src/notification-request.ts +6 -2
  20. package/src/pack-contents.test.ts +53 -17
  21. package/src/qualified-value.test.ts +51 -3
  22. package/src/qualified-value.ts +54 -1
  23. package/src/self-contained-pack.test.ts +115 -0
  24. package/src/stdio-startup.test.ts +223 -5
  25. package/src/team-frame.test.ts +86 -0
  26. package/src/team-frame.ts +78 -0
  27. package/src/time-normalization.test.ts +94 -0
  28. package/src/time-normalization.ts +90 -0
  29. package/vitest.config.ts +7 -0
  30. package/dist/cli.d.ts +0 -2
  31. package/dist/element-format.d.ts +0 -8
  32. package/dist/element-format.js +0 -18
  33. package/dist/element-format.test.d.ts +0 -1
  34. package/dist/element-format.test.js +0 -20
  35. package/dist/export-video.test.d.ts +0 -1
  36. package/dist/export-video.test.js +0 -156
  37. package/dist/http-server.d.ts +0 -65
  38. package/dist/http-server.test.d.ts +0 -1
  39. package/dist/http-server.test.js +0 -232
  40. package/dist/index.d.ts +0 -2
  41. package/dist/notification-request.d.ts +0 -3
  42. package/dist/notification-request.js +0 -25
  43. package/dist/notification-request.test.d.ts +0 -1
  44. package/dist/notification-request.test.js +0 -36
  45. package/dist/pack-contents.test.d.ts +0 -1
  46. package/dist/pack-contents.test.js +0 -119
  47. package/dist/qualified-value.d.ts +0 -10
  48. package/dist/qualified-value.js +0 -18
  49. package/dist/qualified-value.test.d.ts +0 -1
  50. package/dist/qualified-value.test.js +0 -20
  51. package/dist/stdio-startup.test.d.ts +0 -1
  52. package/dist/stdio-startup.test.js +0 -273
  53. package/dist/team-config.d.ts +0 -19
  54. package/dist/team-config.js +0 -99
  55. package/dist/team-config.test.d.ts +0 -1
  56. package/dist/team-config.test.js +0 -90
  57. package/dist/telemetry.d.ts +0 -24
  58. package/dist/telemetry.js +0 -228
  59. package/dist/telemetry.test.d.ts +0 -1
  60. package/dist/telemetry.test.js +0 -130
  61. package/dist/version.d.ts +0 -1
  62. package/dist/version.js +0 -27
  63. package/dist/version.test.d.ts +0 -1
  64. package/dist/version.test.js +0 -144
package/src/index.ts CHANGED
@@ -26,10 +26,21 @@ import {
26
26
  flushMcpTelemetry,
27
27
  initMcpTelemetry,
28
28
  } from "./telemetry";
29
- import { createMcpQualifiedValueReporter } from "./qualified-value";
29
+ import {
30
+ createMcpQualifiedValueReporter,
31
+ resolveMcpClient,
32
+ } from "./qualified-value";
30
33
  import { discoverTeamApiBase, discoverTeamToken } from "./team-config";
34
+ import { teamFrameContent, teamFramePath } from "./team-frame";
31
35
  import { PKG_VERSION } from "./version";
32
36
  import { formatForElementPurpose } from "./element-format";
37
+ import { buildActivitySummaryResult } from "./activity-summary-tool";
38
+ import {
39
+ localContextDayStarts,
40
+ normalizeTime,
41
+ normalizeTimeFields,
42
+ } from "./time-normalization";
43
+ import { resolveScreenpipeApiBase } from "./api-base";
33
44
 
34
45
  initMcpTelemetry({ transport: "stdio" });
35
46
 
@@ -58,14 +69,11 @@ for (let i = 0; i < args.length; i++) {
58
69
  // screenpipe (e.g. an agent on a VPS reading a synced copy of your data),
59
70
  // not just localhost. Priority:
60
71
  // 1. --screenpipe-url / --screenpipe-api-url flag
61
- // 2. SCREENPIPE_API_URL env (set by `screenpipe agent setup --api-url`)
62
- // 3. --screenpipe-host (+ --port) → http://host:port
63
- // 4. default http://localhost:<port>
64
- const SCREENPIPE_API = (
65
- baseOverride ||
66
- process.env.SCREENPIPE_API_URL ||
67
- `http://${host}:${port}`
68
- ).replace(/\/+$/, "");
72
+ // 2. SCREENPIPE_LOCAL_API_URL / PORT from the launching desktop instance
73
+ // 3. SCREENPIPE_API_URL env (set by `screenpipe agent setup --api-url`)
74
+ // 4. --screenpipe-host (+ --port) → http://host:port
75
+ // 5. default http://localhost:<port>
76
+ const SCREENPIPE_API = resolveScreenpipeApiBase({ baseOverride, host, port });
69
77
 
70
78
  // Discover the local API key, in priority order:
71
79
  //
@@ -78,14 +86,13 @@ const SCREENPIPE_API = (
78
86
  // 3. CLI via node-adjacent npx — for dev environments that have node but
79
87
  // not the desktop app.
80
88
  // 4. CLI via PATH-based npx — last CLI fallback.
81
- // 5. Direct sqlite3 read of ~/.screenpipe/db.sqlite — plaintext entries
82
- // only (encrypted entries need the keychain, which only the CLI can
83
- // reach). Kept as a final last-resort for users who have screenpipe
84
- // *data* but no working CLI install (rare). Demoted below the CLI
85
- // paths because it reimplements logic that lives in `auth_key.rs` and
86
- // can silently drift on storage-format changes.
87
89
  //
88
- // If all 5 miss we log a loud stderr warning so it surfaces in the host's
90
+ // The MCP process never opens Screenpipe's SQLite files. The desktop app and
91
+ // CLI own database access, locking, WAL handling, and secret-store decoding;
92
+ // bypassing those boundaries here can race the recorder or drift from the
93
+ // encrypted storage format.
94
+ //
95
+ // If all 4 miss we log a loud stderr warning so it surfaces in the host's
89
96
  // MCP log instead of the user just seeing 403s with no explanation.
90
97
  async function discoverApiKey(): Promise<string> {
91
98
  const envKey = process.env.SCREENPIPE_LOCAL_API_KEY || process.env.SCREENPIPE_API_KEY;
@@ -216,51 +223,7 @@ async function discoverApiKey(): Promise<string> {
216
223
  }
217
224
  } catch {}
218
225
 
219
- // 5. Direct sqlite3 read of the secret store (last-resort). Plaintext
220
- // entries only — encrypted ones live behind the keychain, which the
221
- // CLI paths above already cover. Used when the user has screenpipe
222
- // data on disk but no working CLI install.
223
- const sqliteCandidates: string[] =
224
- process.platform === "win32"
225
- ? ["sqlite3.exe", "C:\\Windows\\System32\\sqlite3.exe"]
226
- : process.platform === "darwin"
227
- ? ["sqlite3", "/usr/bin/sqlite3", "/opt/homebrew/bin/sqlite3", "/usr/local/bin/sqlite3"]
228
- : ["sqlite3", "/usr/bin/sqlite3", "/usr/local/bin/sqlite3"];
229
- try {
230
- const dbPath = path.join(home, ".screenpipe", "db.sqlite");
231
- if (fs.existsSync(dbPath)) {
232
- let row: string | null = null;
233
- for (const candidate of sqliteCandidates) {
234
- if (budgetLeft() <= 0) break;
235
- try {
236
- const { stdout } = await execFileAsync(
237
- candidate,
238
- [dbPath, "SELECT hex(nonce), value FROM secrets WHERE key = 'api_auth_key';"],
239
- { timeout: Math.min(5000, budgetLeft()), encoding: "utf-8" },
240
- );
241
- row = String(stdout).trim();
242
- break;
243
- } catch {
244
- // try next candidate
245
- }
246
- }
247
- if (row) {
248
- const sepIdx = row.indexOf("|");
249
- const nonceHex = sepIdx >= 0 ? row.substring(0, sepIdx) : "";
250
- const value = sepIdx >= 0 ? row.substring(sepIdx + 1) : row;
251
- const isPlaintext = !nonceHex || /^0+$/.test(nonceHex);
252
- if (isPlaintext && value) {
253
- const decoded = Buffer.from(value, "base64").toString("utf-8");
254
- if (decoded && decoded.startsWith("sp-")) return decoded;
255
- if (value.startsWith("sp-")) return value;
256
- }
257
- // Encrypted — only the CLI paths above can decrypt this; we
258
- // already tried them.
259
- }
260
- }
261
- } catch {}
262
-
263
- // All five paths missed. Log loudly to stderr so the host's MCP
226
+ // All four paths missed. Log loudly to stderr so the host's MCP
264
227
  // panel surfaces this instead of the user seeing cryptic 403s from
265
228
  // the screenpipe server on every tool call.
266
229
  process.stderr.write(
@@ -269,14 +232,14 @@ async function discoverApiKey(): Promise<string> {
269
232
  " - env vars (SCREENPIPE_LOCAL_API_KEY / SCREENPIPE_API_KEY) not set",
270
233
  " - bundled `bun` from screenpipe.app not found at any known install path",
271
234
  " - npx fallback unavailable",
272
- " - direct sqlite3 read of ~/.screenpipe/db.sqlite failed",
273
235
  "Fix: set SCREENPIPE_LOCAL_API_KEY in your MCP launcher's env block,",
274
- "or install the screenpipe desktop app (https://screenpi.pe).",
236
+ "or install the screenpipe desktop app (https://screenpi.pe) so its CLI",
237
+ "can resolve the key without bypassing Screenpipe's database boundary.",
275
238
  "",
276
239
  ].join("\n"),
277
240
  );
278
- // This is a user-side misconfiguration (no key set + no desktop app / CLI /
279
- // local DB), not a screenpipe defect — the stderr hint above tells the user
241
+ // This is a user-side misconfiguration (no key set + no desktop app / CLI),
242
+ // not a screenpipe defect — the stderr hint above tells the user
280
243
  // how to fix it. Log it as `info` for activation signal, and throttle to one
281
244
  // event per machine per day so a respawning MCP host can't escalate it.
282
245
  captureMcpMessage("api key discovery failed", "info", {
@@ -287,7 +250,7 @@ async function discoverApiKey(): Promise<string> {
287
250
  }
288
251
 
289
252
  // API key is resolved LAZILY, never at module load. `discoverApiKey()` can run
290
- // several subprocess fallbacks (bundled bun, npx, sqlite) that, on a cold cache
253
+ // several subprocess fallbacks (bundled bun and npx) that, on a cold cache
291
254
  // or restricted PATH, take many seconds. Running that synchronously at module
292
255
  // scope used to block the entire module body from finishing — which meant
293
256
  // `main()` (and therefore `server.connect()`) was never reached until discovery
@@ -388,7 +351,7 @@ const TOOLS: Tool[] = [
388
351
  properties: {
389
352
  q: {
390
353
  type: "string",
391
- description: "Full-text search query. Omit to return all content in time range. Avoid for audio — transcriptions are noisy, q filters too aggressively.",
354
+ description: "Full-text search query. Omit to return all content in time range. Avoid for audio — transcriptions are noisy, q filters too aggressively. For an attached activity episode, its generated title/summary are labels, not query terms: use the exact time range and artifact anchors with q omitted.",
392
355
  },
393
356
  content_type: {
394
357
  type: "string",
@@ -401,11 +364,11 @@ const TOOLS: Tool[] = [
401
364
  offset: { type: "integer", description: "Pagination offset. Use when results say 'use offset=N for more'.", default: 0 },
402
365
  start_time: {
403
366
  type: "string",
404
- description: "Accepted: ISO 8601 ('2024-01-15T10:00:00Z'), 'Nh ago' / 'Nd ago' / 'Nw ago', 'now', 'yesterday', 'today', or bare 'YYYY-MM-DD'. Always provide to avoid scanning entire history.",
367
+ description: "Accepted: ISO 8601 ('2024-01-15T10:00:00Z'), relative time, or local calendar ('today', 'yesterday', 'tomorrow', 'YYYY-MM-DD'). Always provide to avoid scanning entire history.",
405
368
  },
406
369
  end_time: {
407
370
  type: "string",
408
- description: "ISO 8601 UTC or relative (e.g. 'now'). Defaults to now.",
371
+ description: "ISO 8601, relative time, or local calendar ('today', 'yesterday', 'tomorrow', 'YYYY-MM-DD'). Defaults to now.",
409
372
  },
410
373
  app_name: { type: "string", description: "Filter by app name (e.g. 'Google Chrome', 'Slack', 'zoom.us'). Case-sensitive." },
411
374
  window_name: { type: "string", description: "Filter by window title substring" },
@@ -438,6 +401,36 @@ const TOOLS: Tool[] = [
438
401
  },
439
402
  },
440
403
  },
404
+ {
405
+ name: "synced-devices",
406
+ description:
407
+ "List this signed-in user's Screenpipe devices that have uploaded Data Sync records, including each device name and last sync time. " +
408
+ "USE WHEN: the user asks what devices are available, names another device, or asks a cross-device question and you need the exact device_name filter. " +
409
+ "This never accepts an account or bucket identifier; the local app forwards the signed-in user's identity.",
410
+ annotations: { title: "Synced Devices", readOnlyHint: true, openWorldHint: true, idempotentHint: true },
411
+ inputSchema: { type: "object", properties: {} },
412
+ },
413
+ {
414
+ name: "search-synced-content",
415
+ description:
416
+ "Search Data Sync records from this signed-in user's devices. Results include device name, device ID, and timestamp for attribution. " +
417
+ "USE WHEN: the user asks about another/named device, asks across devices, or local search does not cover the requested machine. " +
418
+ "For the current machine only, use search-content. Start with a narrow time range and limit=10.",
419
+ annotations: { title: "Search Synced Content", readOnlyHint: true, openWorldHint: true, idempotentHint: true },
420
+ inputSchema: {
421
+ type: "object",
422
+ properties: {
423
+ q: { type: "string", description: "Case-insensitive substring query. Omit to return all matching records in the window." },
424
+ device_name: { type: "string", description: "Exact device name from synced-devices." },
425
+ device_id: { type: "string", description: "Exact device ID from synced-devices." },
426
+ app_name: { type: "string", description: "Exact app name, case-insensitive." },
427
+ since: { type: "string", description: "ISO 8601 lower bound." },
428
+ until: { type: "string", description: "ISO 8601 upper bound." },
429
+ since_hours_ago: { type: "number", description: "Alternative relative time window in hours." },
430
+ limit: { type: "integer", description: "Max results (default 50, max 200).", default: 50 },
431
+ },
432
+ },
433
+ },
441
434
  {
442
435
  name: "list-meetings",
443
436
  description:
@@ -451,8 +444,8 @@ const TOOLS: Tool[] = [
451
444
  inputSchema: {
452
445
  type: "object",
453
446
  properties: {
454
- start_time: { type: "string", description: "ISO 8601 UTC or relative (e.g. '1d ago'). Omit when searching by q — it filters all history." },
455
- end_time: { type: "string", description: "ISO 8601 UTC or relative" },
447
+ start_time: { type: "string", description: "ISO 8601, relative time, or local calendar ('today', 'yesterday', 'tomorrow', 'YYYY-MM-DD'). Omit when searching by q — it filters all history." },
448
+ end_time: { type: "string", description: "ISO 8601, relative time, or local calendar ('today', 'yesterday', 'tomorrow', 'YYYY-MM-DD')" },
456
449
  q: { type: "string", description: "Case-insensitive substring filter on title, attendees (names/emails), and note. Searches all history." },
457
450
  limit: { type: "integer", description: "Max results (default 20)", default: 20 },
458
451
  offset: { type: "integer", description: "Pagination offset", default: 0 },
@@ -462,17 +455,31 @@ const TOOLS: Tool[] = [
462
455
  {
463
456
  name: "activity-summary",
464
457
  description:
465
- "Rich activity overview: app usage, window/tab titles with URLs and time spent, key text per context, audio transcriptions. " +
458
+ "Rich activity overview: authoritative active minutes, app/window time, edited document paths, key text, and audio transcriptions, with optional parsed task context when available. " +
466
459
  "USE WHEN: any broad question about what the user did — 'what was I doing?', 'how long on X?', 'which apps?', 'recap my morning'. " +
467
460
  "This is almost always the right first call for time-range questions — usually sufficient without follow-up searches. " +
461
+ "Use parsed/path evidence to identify tasks, but only active-minute fields for duration; frame and row counts are never time. " +
468
462
  "DO NOT USE for: finding a specific keyword (use keyword-search) or a specific UI control (use search-elements).",
469
463
  annotations: { title: "Activity Summary", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
470
464
  inputSchema: {
471
465
  type: "object",
472
466
  properties: {
473
- start_time: { type: "string", description: "ISO 8601 UTC or relative (e.g. '3h ago')" },
474
- end_time: { type: "string", description: "ISO 8601 UTC or relative (e.g. 'now')" },
467
+ start_time: { type: "string", description: "ISO 8601, relative (e.g. '3h ago'), or local calendar ('today', 'yesterday', 'tomorrow', 'YYYY-MM-DD')" },
468
+ end_time: { type: "string", description: "ISO 8601, relative (e.g. 'now'), or local calendar ('today', 'yesterday', 'tomorrow', 'YYYY-MM-DD')" },
475
469
  app_name: { type: "string", description: "Optional app name filter to focus on one app" },
470
+ include_parsed_context: {
471
+ type: "boolean",
472
+ description:
473
+ "Optionally include a bounded parsed-context sample for identifying projects and tasks. Parsed capture is experimental and may be disabled or unsupported. Context only; never use row counts as duration.",
474
+ default: false,
475
+ },
476
+ parsed_context_limit: {
477
+ type: "integer",
478
+ minimum: 1,
479
+ maximum: 20,
480
+ description: "Maximum parsed-context rows (default 10, max 20).",
481
+ default: 10,
482
+ },
476
483
  },
477
484
  required: ["start_time", "end_time"],
478
485
  },
@@ -495,8 +502,8 @@ const TOOLS: Tool[] = [
495
502
  description: "Element source. 'accessibility' is preferred (OS-native tree). 'ocr' for apps without a11y.",
496
503
  },
497
504
  role: { type: "string", description: "Element role filter (e.g. 'AXButton', 'AXLink', 'AXTextField')" },
498
- start_time: { type: "string", description: "ISO 8601 UTC or relative" },
499
- end_time: { type: "string", description: "ISO 8601 UTC or relative" },
505
+ start_time: { type: "string", description: "ISO 8601, relative time, or local calendar ('today', 'yesterday', 'tomorrow', 'YYYY-MM-DD')" },
506
+ end_time: { type: "string", description: "ISO 8601, relative time, or local calendar ('today', 'yesterday', 'tomorrow', 'YYYY-MM-DD')" },
500
507
  app_name: { type: "string", description: "Filter by app name" },
501
508
  purpose: {
502
509
  type: "string",
@@ -534,8 +541,8 @@ const TOOLS: Tool[] = [
534
541
  inputSchema: {
535
542
  type: "object",
536
543
  properties: {
537
- start_time: { type: "string", description: 'ISO 8601 UTC or relative (e.g. "5m ago", "now")' },
538
- end_time: { type: "string", description: 'ISO 8601 UTC or relative (e.g. "5m ago", "now")' },
544
+ start_time: { type: "string", description: "ISO 8601, relative time, or local calendar ('today', 'yesterday', 'tomorrow', 'YYYY-MM-DD')" },
545
+ end_time: { type: "string", description: "ISO 8601, relative time, or local calendar ('today', 'yesterday', 'tomorrow', 'YYYY-MM-DD')" },
539
546
  output_path: {
540
547
  type: "string",
541
548
  description:
@@ -565,11 +572,54 @@ const TOOLS: Tool[] = [
565
572
  },
566
573
  },
567
574
  },
575
+ {
576
+ name: "get-feedback",
577
+ description:
578
+ "Search local user ratings and written comments attached to AI-produced notifications, chats, memories, blocks, artifacts, and other targets. " +
579
+ "Use before generating related work so you preserve what earned up ratings and correct what earned down ratings.",
580
+ annotations: { title: "Get AI Feedback", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
581
+ inputSchema: {
582
+ type: "object",
583
+ properties: {
584
+ kind: {
585
+ type: "string",
586
+ description: "Optional target kind, such as notification, chat, memory, block, artifact, or structured_output.",
587
+ },
588
+ target_id: {
589
+ type: "string",
590
+ description: "Optional exact target id.",
591
+ },
592
+ producer: {
593
+ type: "string",
594
+ description: "Optional producer reference, for example pipe:daily-recap. Pipe tokens are always restricted to themselves.",
595
+ },
596
+ rating: {
597
+ type: "string",
598
+ enum: ["up", "down"],
599
+ description: "Optional rating filter.",
600
+ },
601
+ q: {
602
+ type: "string",
603
+ description: "Optional text search across comments, snapshots, target ids, producers, and context.",
604
+ },
605
+ since: {
606
+ type: "string",
607
+ description: "Optional RFC3339 lower bound on updated_at.",
608
+ },
609
+ limit: {
610
+ type: "integer",
611
+ description: "Maximum records (default 50, max 500).",
612
+ default: 50,
613
+ },
614
+ },
615
+ },
616
+ },
568
617
  {
569
618
  name: "send-notification",
570
619
  description:
571
620
  "Send a notification to the screenpipe desktop UI. " +
572
- "Use to alert the user about findings, completed tasks, or actions needing attention.",
621
+ "Use high priority only for time-sensitive failures or decisions needing human attention; " +
622
+ "routine findings and completed tasks should be normal or low.",
573
623
  annotations: { title: "Send Notification", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
574
624
  inputSchema: {
575
625
  type: "object",
@@ -577,6 +627,12 @@ const TOOLS: Tool[] = [
577
627
  title: { type: "string", description: "Notification title (short, descriptive)" },
578
628
  body: { type: "string", description: "Notification body (markdown supported)" },
579
629
  pipe_name: { type: "string", description: "Name of the pipe/tool sending this notification" },
630
+ priority: {
631
+ type: "string",
632
+ enum: ["high", "normal", "low"],
633
+ description: "Every priority appears in the top-right panel. High also appears in the focused Priority view, normal (default) stays in All, and low is toast-only by default.",
634
+ default: "normal",
635
+ },
580
636
  timeout_secs: { type: "integer", description: "Auto-dismiss after N seconds (default 20). Use 0 for persistent.", default: 20 },
581
637
  actions: {
582
638
  type: "array",
@@ -771,8 +827,8 @@ const TOOLS: Tool[] = [
771
827
  type: "object",
772
828
  properties: {
773
829
  q: { type: "string", description: "Keyword query (FTS5 syntax: quoted phrases, AND/OR, prefix*)" },
774
- start_time: { type: "string", description: "ISO 8601 UTC, 'Nh ago' / 'Nd ago' / 'Nw ago', 'now', 'yesterday', 'today', or 'YYYY-MM-DD'" },
775
- end_time: { type: "string", description: "Same formats as start_time" },
830
+ start_time: { type: "string", description: "ISO 8601, relative time, or local calendar ('today', 'yesterday', 'tomorrow', 'YYYY-MM-DD')" },
831
+ end_time: { type: "string", description: "ISO 8601, relative time, or local calendar ('today', 'yesterday', 'tomorrow', 'YYYY-MM-DD')" },
776
832
  app_name: { type: "string", description: "Filter by exact app name (case-sensitive, e.g. 'Google Chrome')" },
777
833
  limit: { type: "integer", description: "Max results (default 20)", default: 20 },
778
834
  offset: { type: "integer", description: "Pagination offset", default: 0 },
@@ -943,7 +999,7 @@ const TEAM_TOOLS: Tool[] = [
943
999
  name: "team-records",
944
1000
  description:
945
1001
  "Chronological dump of the org's data for a time window — both raw " +
946
- "telemetry (frame/audio) and the structured outputs of the enterprise-" +
1002
+ "telemetry (frame/parsed/audio/feedback) and the structured outputs of the enterprise-" +
947
1003
  "worker pipes (sop/skill/trajectory/memory/workflow). " +
948
1004
  "Raw kinds return oldest → newest (vs team-search which is recency-ranked). " +
949
1005
  "Synthesized kinds return one record per device's latest run by default " +
@@ -960,9 +1016,9 @@ const TEAM_TOOLS: Tool[] = [
960
1016
  device_id: { type: "string", description: "Restrict to one device (optional). Raw kinds only." },
961
1017
  kind: {
962
1018
  type: "string",
963
- enum: ["frame", "audio", "all", "sop", "skill", "trajectory", "memory", "workflow"],
1019
+ enum: ["frame", "parsed", "audio", "feedback", "all", "sop", "skill", "trajectory", "memory", "workflow"],
964
1020
  description:
965
- "What to return. Raw: frame|audio|all (telemetry). " +
1021
+ "What to return. Raw: frame|parsed|audio|feedback|all (telemetry and human feedback). " +
966
1022
  "Synthesized: sop|skill|trajectory|memory|workflow (pipe outputs). " +
967
1023
  "Default: all.",
968
1024
  default: "all",
@@ -981,6 +1037,36 @@ const TEAM_TOOLS: Tool[] = [
981
1037
  },
982
1038
  },
983
1039
  },
1040
+ {
1041
+ name: "team-frame",
1042
+ description:
1043
+ "Read one PII-redacted team screenshot. Use device_id and frame_id from " +
1044
+ "team-search or team-records. Returns actual JPEG image content when the " +
1045
+ "device has uploaded it, or an explicit unavailable result. Never claim " +
1046
+ "to have seen a frame unless this tool returns image content. " +
1047
+ "Auth: enterprise admin token with read:records.",
1048
+ annotations: { title: "Team Frame", readOnlyHint: true, openWorldHint: true, idempotentHint: true },
1049
+ inputSchema: {
1050
+ type: "object",
1051
+ properties: {
1052
+ device_id: {
1053
+ type: "string",
1054
+ minLength: 1,
1055
+ maxLength: 64,
1056
+ pattern: "^[A-Za-z0-9_-]+$",
1057
+ description: "Device ID from team-search or team-devices.",
1058
+ },
1059
+ frame_id: {
1060
+ type: "integer",
1061
+ minimum: 1,
1062
+ maximum: 999999999999999,
1063
+ description: "Frame ID from team-search or team-records.",
1064
+ },
1065
+ },
1066
+ required: ["device_id", "frame_id"],
1067
+ additionalProperties: false,
1068
+ },
1069
+ },
984
1070
  ];
985
1071
 
986
1072
  // Pipe-output kinds map to /workflows/generated, raw kinds map to /records.
@@ -1028,6 +1114,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1028
1114
  if (uri === "screenpipe://context") {
1029
1115
  const now = new Date();
1030
1116
  const ms = now.getTime();
1117
+ const dayStarts = localContextDayStarts(now);
1031
1118
  return {
1032
1119
  contents: [
1033
1120
  {
@@ -1047,8 +1134,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1047
1134
  now: now.toISOString(),
1048
1135
  one_hour_ago: new Date(ms - 60 * 60 * 1000).toISOString(),
1049
1136
  three_hours_ago: new Date(ms - 3 * 60 * 60 * 1000).toISOString(),
1050
- today_start: `${now.toISOString().split("T")[0]}T00:00:00Z`,
1051
- yesterday_start: `${new Date(ms - 24 * 60 * 60 * 1000).toISOString().split("T")[0]}T00:00:00Z`,
1137
+ ...dayStarts,
1052
1138
  one_week_ago: new Date(ms - 7 * 24 * 60 * 60 * 1000).toISOString(),
1053
1139
  },
1054
1140
  },
@@ -1077,6 +1163,10 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1077
1163
  | 3 | search-elements | Need UI structure: buttons, links, form fields |
1078
1164
  | 4 | frame-context | Need full detail for a specific moment (use frame_id from step 2) |
1079
1165
 
1166
+ For another/named device or an across-device question, use synced-devices to
1167
+ resolve the device name, then search-synced-content. Keep search-content for the
1168
+ current machine. Synced results must be attributed with their device and timestamp.
1169
+
1080
1170
  ## Search Strategy
1081
1171
 
1082
1172
  - **Always provide start_time** — without it, search scans the entire history
@@ -1092,6 +1182,8 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1092
1182
  - "What did I discuss in my meeting?" → list-meetings to find it, then get-meeting with include_transcript=true
1093
1183
  - "When did I last talk to <person>?" → list-meetings with q=<name or email>, NO start_time (q searches all history)
1094
1184
  - "Find when I was on Twitter" → search-content with app_name='Arc' (or the browser name), q='twitter'
1185
+ - "What was I doing on my MacBook this morning?" → synced-devices, then search-synced-content with device_name='MacBook' and the requested time window
1186
+ - "Find this across my devices" → search-synced-content with the requested time window and no device filter
1095
1187
  - "Remember that I prefer X" → update-memory with content describing the preference
1096
1188
  - "What do you remember about X?" → search-content with content_type='memory', q='X'
1097
1189
  - "Automate X every day / on a schedule" → read the screenpipe://guide/pipes resource, then create-pipe (a scheduled AI automation)
@@ -1101,6 +1193,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1101
1193
  When referencing specific moments in results, create clickable links:
1102
1194
  - Frame: [10:30 AM — Chrome](screenpipe://frame/{frame_id}) — use frame_id from search results
1103
1195
  - Timeline: [meeting at 3pm](screenpipe://timeline?timestamp=2024-01-15T15:00:00Z) — use exact timestamp from results
1196
+ - Chat: [crm](screenpipe://chat/{conversationId}) — use a real conversation id
1104
1197
  Never fabricate IDs or timestamps — only use values from actual results.
1105
1198
  `,
1106
1199
  },
@@ -1230,6 +1323,7 @@ async function fetchAPI(
1230
1323
  headers: {
1231
1324
  "Content-Type": "application/json",
1232
1325
  ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
1326
+ "x-screenpipe-client": "mcp",
1233
1327
  ...options.headers,
1234
1328
  },
1235
1329
  });
@@ -1259,44 +1353,13 @@ const qualifiedValue = createMcpQualifiedValueReporter((payload) =>
1259
1353
  method: "POST",
1260
1354
  body: JSON.stringify(payload),
1261
1355
  }),
1356
+ () =>
1357
+ resolveMcpClient(
1358
+ process.env.SCREENPIPE_MCP_CLIENT,
1359
+ server.getClientVersion()?.name,
1360
+ ),
1262
1361
  );
1263
1362
 
1264
- // Server's deserialize_flexible_datetime accepts ISO 8601 + "Nh ago" / "Nd ago"
1265
- // / "Nw ago" / "now". Models also try "yesterday", "today", and bare dates
1266
- // ("2026-05-17") — normalize those here so the request doesn't 400.
1267
- function normalizeTime(input: string | undefined): string | undefined {
1268
- if (!input) return input;
1269
- const s = input.trim();
1270
- if (!s) return input;
1271
- const lower = s.toLowerCase();
1272
- if (lower === "yesterday") return "1d ago";
1273
- if (lower === "today") {
1274
- return `${new Date().toISOString().split("T")[0]}T00:00:00Z`;
1275
- }
1276
- if (lower === "tomorrow") {
1277
- const t = new Date();
1278
- t.setUTCDate(t.getUTCDate() + 1);
1279
- return `${t.toISOString().split("T")[0]}T00:00:00Z`;
1280
- }
1281
- // Bare YYYY-MM-DD → start of day UTC
1282
- if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return `${s}T00:00:00Z`;
1283
- return s;
1284
- }
1285
-
1286
- // Apply normalizeTime to start_time/end_time fields in an args object.
1287
- // Returns a new object — does not mutate the input.
1288
- function normalizeTimeFields(
1289
- args: Record<string, unknown>,
1290
- ): Record<string, unknown> {
1291
- const out = { ...args };
1292
- for (const k of ["start_time", "end_time"] as const) {
1293
- if (typeof out[k] === "string") {
1294
- out[k] = normalizeTime(out[k] as string);
1295
- }
1296
- }
1297
- return out;
1298
- }
1299
-
1300
1363
  // Zone label for a timestamp's HH:MM slice. The server serializes timestamps in
1301
1364
  // its LOCAL timezone (e.g. "...T09:03:44+05:30"), so the HH:MM is already local —
1302
1365
  // derive the label from the string's own offset instead of hardcoding "UTC"
@@ -1476,6 +1539,69 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1476
1539
  return { content: [{ type: "text", text: trimmed || "(no logs yet)" }] };
1477
1540
  }
1478
1541
 
1542
+ case "synced-devices": {
1543
+ const response = await callAPI("/data-sync/devices");
1544
+ const data = await response.json();
1545
+ const devices = Array.isArray(data.devices) ? data.devices : [];
1546
+ if (devices.length === 0) {
1547
+ return {
1548
+ content: [{
1549
+ type: "text",
1550
+ text: data.enabled === false
1551
+ ? "Data Sync is off and no synced devices are available. Enable Data Sync in Screenpipe settings on the devices you want to query."
1552
+ : "No synced devices are available yet. Enable Data Sync and let a device complete its first upload.",
1553
+ }],
1554
+ };
1555
+ }
1556
+ return {
1557
+ content: [{
1558
+ type: "text",
1559
+ text: devices
1560
+ .map((device: any) =>
1561
+ `${device.device_name} (${device.device_id}) — last synced ${device.last_synced_at}` +
1562
+ `${device.platform ? ` — ${device.platform}` : ""}`
1563
+ )
1564
+ .join("\n"),
1565
+ }],
1566
+ };
1567
+ }
1568
+
1569
+ case "search-synced-content": {
1570
+ const params = new URLSearchParams();
1571
+ for (const key of ["q", "device_name", "device_id", "app_name", "since", "until", "since_hours_ago", "limit"]) {
1572
+ const value = args[key];
1573
+ if (value !== null && value !== undefined && value !== "") {
1574
+ params.set(key, String(value));
1575
+ }
1576
+ }
1577
+ const response = await callAPI(`/data-sync/search?${params.toString()}`);
1578
+ const data = await response.json();
1579
+ const results = Array.isArray(data.results) ? data.results : [];
1580
+ if (results.length === 0) {
1581
+ return {
1582
+ content: [{
1583
+ type: "text",
1584
+ text: data.enabled === false
1585
+ ? "No matching synced records. Data Sync is currently off, so no new records are uploading."
1586
+ : "No matching synced records. Try a wider time range, confirm the device with synced-devices, or use a broader query.",
1587
+ }],
1588
+ };
1589
+ }
1590
+ const prefix = data.truncated
1591
+ ? "Results are truncated; narrow the device, time range, or query.\n\n"
1592
+ : "";
1593
+ return {
1594
+ content: [{
1595
+ type: "text",
1596
+ text: prefix + results.map((record: any) => {
1597
+ const content = record.text || record.transcription || record.content || "";
1598
+ return `[${record.device || record.device_id || "unknown device"}] ${record.t || "unknown time"}` +
1599
+ `${record.app ? ` — ${record.app}` : ""}\n${truncateMiddle(String(content), DEFAULT_SEARCH_CONTENT_TRUNCATE)}`;
1600
+ }).join("\n\n"),
1601
+ }],
1602
+ };
1603
+ }
1604
+
1479
1605
  case "search-content": {
1480
1606
  const includeFrames = args.include_frames === true;
1481
1607
  const normalized = normalizeTimeFields(args);
@@ -1656,94 +1782,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1656
1782
  }
1657
1783
 
1658
1784
  case "activity-summary": {
1659
- const normalized = normalizeTimeFields(args);
1660
- const params = new URLSearchParams();
1661
- for (const [key, value] of Object.entries(normalized)) {
1662
- if (value !== null && value !== undefined) {
1663
- params.append(key, String(value));
1664
- }
1665
- }
1666
-
1667
- const response = await callAPI(`/activity-summary?${params.toString()}`);
1668
-
1669
- const data = await response.json();
1670
-
1671
- if (
1672
- (data.total_frames ?? 0) > 0 ||
1673
- (data.audio_summary?.segment_count ?? 0) > 0 ||
1674
- (data.apps?.length ?? 0) > 0
1675
- ) {
1785
+ const result = await buildActivitySummaryResult(args, callAPI);
1786
+ if (result.hasArtifact) {
1676
1787
  qualifiedValue.artifactResult();
1677
1788
  }
1678
-
1679
- const appsLines = (data.apps || []).map(
1680
- (a: {
1681
- name: string;
1682
- frame_count: number;
1683
- minutes: number;
1684
- first_seen?: string;
1685
- last_seen?: string;
1686
- }) => {
1687
- const timeSpan =
1688
- a.first_seen && a.last_seen
1689
- ? `, ${a.first_seen.slice(11, 16)}–${a.last_seen.slice(11, 16)}${zoneSuffix(a.first_seen)}`
1690
- : "";
1691
- return ` ${a.name}: ${a.minutes} min (${a.frame_count} frames${timeSpan})`;
1692
- }
1693
- );
1694
-
1695
- // Window/tab activity — what pages/documents were open
1696
- const windowLines = (data.windows || []).map(
1697
- (w: {
1698
- app_name: string;
1699
- window_name: string;
1700
- browser_url: string;
1701
- minutes: number;
1702
- frame_count: number;
1703
- }) => {
1704
- const url = w.browser_url ? ` (${w.browser_url})` : "";
1705
- return ` [${w.app_name}] ${w.window_name}${url} — ${w.minutes} min`;
1706
- }
1707
- );
1708
-
1709
- const speakerLines = (data.audio_summary?.speakers || []).map(
1710
- (s: { name: string; segment_count: number }) =>
1711
- ` ${s.name}: ${s.segment_count} segments`
1712
- );
1713
-
1714
- // Actual audio transcriptions (not just counts)
1715
- const transcriptLines = (data.audio_summary?.top_transcriptions || []).map(
1716
- (t: { transcription: string; speaker: string; device: string; timestamp: string }) =>
1717
- ` [${t.speaker}, ${t.timestamp.slice(11, 19)}] ${t.transcription}`
1718
- );
1719
-
1720
- // Key text content sampled across the time range
1721
- const textLines = (data.key_texts || data.recent_texts || []).map(
1722
- (t: { text: string; app_name: string; window_name?: string; timestamp: string }) => {
1723
- const win = t.window_name ? ` | ${t.window_name}` : "";
1724
- return ` [${t.app_name}${win}, ${t.timestamp.slice(11, 19)}] ${t.text}`;
1725
- }
1726
- );
1727
-
1728
- const summary = [
1729
- `Activity Summary (${data.time_range?.start} → ${data.time_range?.end})`,
1730
- `Total frames: ${data.total_frames}`,
1731
- "",
1732
- "Apps:",
1733
- ...(appsLines.length ? appsLines : [" (none)"]),
1734
- "",
1735
- "Windows & Tabs:",
1736
- ...(windowLines.length ? windowLines.slice(0, 20) : [" (none)"]),
1737
- "",
1738
- `Audio: ${data.audio_summary?.segment_count || 0} segments`,
1739
- ...(speakerLines.length ? speakerLines : []),
1740
- ...(transcriptLines.length ? ["", "Audio transcriptions:", ...transcriptLines.slice(0, 15)] : []),
1741
- "",
1742
- "Key content (sampled across time range):",
1743
- ...(textLines.length ? textLines.slice(0, 20) : [" (none)"]),
1744
- ].join("\n");
1745
-
1746
- return { content: [{ type: "text", text: summary }] };
1789
+ return { content: [{ type: "text", text: result.text }] };
1747
1790
  }
1748
1791
 
1749
1792
  case "search-elements": {
@@ -1822,8 +1865,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1822
1865
  }
1823
1866
 
1824
1867
  case "export-video": {
1825
- const startTime = normalizeTime(args.start_time as string);
1826
- const endTime = normalizeTime(args.end_time as string);
1868
+ const now = new Date();
1869
+ const startTime = normalizeTime(args.start_time as string, now);
1870
+ const endTime = normalizeTime(args.end_time as string, now);
1827
1871
 
1828
1872
  if (!startTime || !endTime) {
1829
1873
  return {
@@ -1964,6 +2008,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1964
2008
  };
1965
2009
  }
1966
2010
 
2011
+ case "get-feedback": {
2012
+ const params = new URLSearchParams();
2013
+ if (typeof args.kind === "string" && args.kind) {
2014
+ params.set("kind", args.kind);
2015
+ }
2016
+ if (typeof args.target_id === "string" && args.target_id) {
2017
+ params.set("target_id", args.target_id);
2018
+ }
2019
+ if (typeof args.producer === "string" && args.producer) {
2020
+ params.set("producer", args.producer);
2021
+ }
2022
+ if (typeof args.rating === "string" && args.rating) {
2023
+ params.set("rating", args.rating);
2024
+ }
2025
+ if (typeof args.q === "string" && args.q) {
2026
+ params.set("q", args.q);
2027
+ }
2028
+ if (typeof args.since === "string" && args.since) {
2029
+ params.set("since", args.since);
2030
+ }
2031
+ if (args.limit !== undefined) {
2032
+ params.set("limit", String(args.limit));
2033
+ }
2034
+ const response = await callAPI(`/feedback${params.size ? `?${params}` : ""}`);
2035
+ const feedback = await response.json();
2036
+ return {
2037
+ content: [{ type: "text", text: JSON.stringify(feedback, null, 2) }],
2038
+ };
2039
+ }
2040
+
1967
2041
  case "health-check": {
1968
2042
  const response = await callAPI("/health");
1969
2043
  const data = await response.json();
@@ -2297,7 +2371,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2297
2371
  // ---------------------------------------------------------------------
2298
2372
  case "team-search":
2299
2373
  case "team-devices":
2300
- case "team-records": {
2374
+ case "team-records":
2375
+ case "team-frame": {
2301
2376
  if (!TEAM_TOKEN) {
2302
2377
  return {
2303
2378
  content: [
@@ -2318,6 +2393,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2318
2393
  ],
2319
2394
  };
2320
2395
  }
2396
+ if (name === "team-frame") {
2397
+ const deviceId = args.device_id;
2398
+ const frameId = args.frame_id;
2399
+ const path = teamFramePath(deviceId, frameId);
2400
+ const response = await fetchTeam(path);
2401
+ return teamFrameContent(response, deviceId as string, frameId as number);
2402
+ }
2321
2403
  // Map MCP tool name → /api/enterprise/v1 path. team-records also
2322
2404
  // routes synthesized pipe outputs (kind=sop|skill|...) to the
2323
2405
  // workflows endpoint so callers see one tool surface for "give me