screenpipe-mcp 0.11.0 → 0.13.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
@@ -53,10 +53,13 @@ for (let i = 0; i < args.length; i++) {
53
53
  }
54
54
  }
55
55
  const SCREENPIPE_API = `http://localhost:${port}`;
56
+ // Read version from package.json (single source of truth)
57
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
58
+ const PKG_VERSION = require("../package.json").version;
56
59
  // Initialize server
57
60
  const server = new index_js_1.Server({
58
61
  name: "screenpipe",
59
- version: "0.9.0",
62
+ version: PKG_VERSION,
60
63
  }, {
61
64
  capabilities: {
62
65
  tools: {},
@@ -251,6 +254,162 @@ const TOOLS = [
251
254
  required: ["title", "pipe_name"],
252
255
  },
253
256
  },
257
+ {
258
+ name: "health-check",
259
+ description: "Check if screenpipe is running and healthy. Returns recording status, frame/audio stats, timestamps.",
260
+ annotations: { title: "Health Check", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
261
+ inputSchema: { type: "object", properties: {} },
262
+ },
263
+ {
264
+ name: "list-audio-devices",
265
+ description: "List available audio input/output devices for recording.",
266
+ annotations: { title: "List Audio Devices", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
267
+ inputSchema: { type: "object", properties: {} },
268
+ },
269
+ {
270
+ name: "list-monitors",
271
+ description: "List available monitors/screens for capture.",
272
+ annotations: { title: "List Monitors", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
273
+ inputSchema: { type: "object", properties: {} },
274
+ },
275
+ {
276
+ name: "add-tags",
277
+ description: "Add tags to a content item (vision frame or audio chunk) for organization and retrieval.",
278
+ annotations: { title: "Add Tags", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
279
+ inputSchema: {
280
+ type: "object",
281
+ properties: {
282
+ content_type: { type: "string", enum: ["vision", "audio"], description: "Type of content to tag" },
283
+ id: { type: "integer", description: "Content item ID" },
284
+ tags: { type: "array", items: { type: "string" }, description: "Tags to add" },
285
+ },
286
+ required: ["content_type", "id", "tags"],
287
+ },
288
+ },
289
+ {
290
+ name: "search-speakers",
291
+ description: "Search for speakers by name prefix. Returns speaker ID, name, and metadata.",
292
+ annotations: { title: "Search Speakers", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
293
+ inputSchema: {
294
+ type: "object",
295
+ properties: {
296
+ name: { type: "string", description: "Speaker name prefix to search for (case-insensitive)" },
297
+ },
298
+ },
299
+ },
300
+ {
301
+ name: "list-unnamed-speakers",
302
+ description: "List speakers that haven't been named yet. Useful for speaker identification workflow.",
303
+ annotations: { title: "List Unnamed Speakers", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
304
+ inputSchema: {
305
+ type: "object",
306
+ properties: {
307
+ limit: { type: "integer", description: "Max results (default 10)", default: 10 },
308
+ offset: { type: "integer", description: "Pagination offset", default: 0 },
309
+ },
310
+ },
311
+ },
312
+ {
313
+ name: "update-speaker",
314
+ description: "Rename a speaker or update their metadata.",
315
+ annotations: { title: "Update Speaker", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
316
+ inputSchema: {
317
+ type: "object",
318
+ properties: {
319
+ id: { type: "integer", description: "Speaker ID" },
320
+ name: { type: "string", description: "New speaker name" },
321
+ metadata: { type: "string", description: "JSON metadata string" },
322
+ },
323
+ required: ["id"],
324
+ },
325
+ },
326
+ {
327
+ name: "merge-speakers",
328
+ description: "Merge two speakers into one (e.g. when the same person was detected as different speakers).",
329
+ annotations: { title: "Merge Speakers", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
330
+ inputSchema: {
331
+ type: "object",
332
+ properties: {
333
+ speaker_to_keep: { type: "integer", description: "Speaker ID to keep" },
334
+ speaker_to_merge: { type: "integer", description: "Speaker ID to merge into the kept one" },
335
+ },
336
+ required: ["speaker_to_keep", "speaker_to_merge"],
337
+ },
338
+ },
339
+ {
340
+ name: "start-meeting",
341
+ description: "Manually start a meeting recording session.",
342
+ annotations: { title: "Start Meeting", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
343
+ inputSchema: {
344
+ type: "object",
345
+ properties: {
346
+ app: { type: "string", description: "App name (default 'manual')", default: "manual" },
347
+ title: { type: "string", description: "Meeting title" },
348
+ attendees: { type: "string", description: "Comma-separated attendee names" },
349
+ },
350
+ },
351
+ },
352
+ {
353
+ name: "stop-meeting",
354
+ description: "Stop the current manual meeting recording session.",
355
+ annotations: { title: "Stop Meeting", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
356
+ inputSchema: { type: "object", properties: {} },
357
+ },
358
+ {
359
+ name: "get-meeting",
360
+ description: "Get details of a specific meeting by ID, including transcription and attendees.",
361
+ annotations: { title: "Get Meeting", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
362
+ inputSchema: {
363
+ type: "object",
364
+ properties: {
365
+ id: { type: "integer", description: "Meeting ID" },
366
+ },
367
+ required: ["id"],
368
+ },
369
+ },
370
+ {
371
+ name: "keyword-search",
372
+ description: "Fast keyword search using FTS index. Faster than search-content for exact keyword matching. " +
373
+ "Returns frame IDs and matched text.",
374
+ annotations: { title: "Keyword Search", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
375
+ inputSchema: {
376
+ type: "object",
377
+ properties: {
378
+ q: { type: "string", description: "Keyword search query" },
379
+ content_type: { type: "string", enum: ["ocr", "audio", "all"], description: "Content type filter", default: "all" },
380
+ start_time: { type: "string", description: "ISO 8601 UTC or relative" },
381
+ end_time: { type: "string", description: "ISO 8601 UTC or relative" },
382
+ app_name: { type: "string", description: "Filter by app name" },
383
+ limit: { type: "integer", description: "Max results (default 20)", default: 20 },
384
+ offset: { type: "integer", description: "Pagination offset", default: 0 },
385
+ },
386
+ required: ["q"],
387
+ },
388
+ },
389
+ {
390
+ name: "get-frame-elements",
391
+ description: "Get all UI elements for a specific frame. More targeted than search-elements when you already have a frame_id.",
392
+ annotations: { title: "Get Frame Elements", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
393
+ inputSchema: {
394
+ type: "object",
395
+ properties: {
396
+ frame_id: { type: "integer", description: "Frame ID" },
397
+ },
398
+ required: ["frame_id"],
399
+ },
400
+ },
401
+ {
402
+ name: "control-recording",
403
+ description: "Start or stop audio/screen recording. Use to pause/resume capture.",
404
+ annotations: { title: "Control Recording", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
405
+ inputSchema: {
406
+ type: "object",
407
+ properties: {
408
+ action: { type: "string", enum: ["start-audio", "stop-audio"], description: "Recording action" },
409
+ },
410
+ required: ["action"],
411
+ },
412
+ },
254
413
  ];
255
414
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
256
415
  return { tools: TOOLS };
@@ -781,6 +940,229 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
781
940
  content: [{ type: "text", text: `Notification sent: ${notifResult.message}` }],
782
941
  };
783
942
  }
943
+ case "health-check": {
944
+ const response = await fetchAPI("/health");
945
+ if (!response.ok)
946
+ throw new Error(`HTTP error: ${response.status}`);
947
+ const data = await response.json();
948
+ return {
949
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
950
+ };
951
+ }
952
+ case "list-audio-devices": {
953
+ const response = await fetchAPI("/audio/list");
954
+ if (!response.ok)
955
+ throw new Error(`HTTP error: ${response.status}`);
956
+ const devices = await response.json();
957
+ if (!Array.isArray(devices) || devices.length === 0) {
958
+ return { content: [{ type: "text", text: "No audio devices found." }] };
959
+ }
960
+ const formatted = devices.map((d) => `${d.is_default ? "* " : " "}${d.name}${d.device_type ? ` (${d.device_type})` : ""}`);
961
+ return {
962
+ content: [{ type: "text", text: `Audio devices:\n${formatted.join("\n")}` }],
963
+ };
964
+ }
965
+ case "list-monitors": {
966
+ const response = await fetchAPI("/vision/list");
967
+ if (!response.ok)
968
+ throw new Error(`HTTP error: ${response.status}`);
969
+ const monitors = await response.json();
970
+ if (!Array.isArray(monitors) || monitors.length === 0) {
971
+ return { content: [{ type: "text", text: "No monitors found." }] };
972
+ }
973
+ const formatted = monitors.map((m) => `${m.is_default ? "* " : " "}Monitor ${m.id}${m.name ? `: ${m.name}` : ""}${m.width ? ` (${m.width}x${m.height})` : ""}`);
974
+ return {
975
+ content: [{ type: "text", text: `Monitors:\n${formatted.join("\n")}` }],
976
+ };
977
+ }
978
+ case "add-tags": {
979
+ const contentType = args.content_type;
980
+ const id = args.id;
981
+ const tags = args.tags;
982
+ if (!contentType || !id || !tags) {
983
+ return { content: [{ type: "text", text: "Error: content_type, id, and tags are required" }] };
984
+ }
985
+ const response = await fetchAPI(`/tags/${contentType}/${id}`, {
986
+ method: "POST",
987
+ body: JSON.stringify({ tags }),
988
+ });
989
+ if (!response.ok)
990
+ throw new Error(`HTTP error: ${response.status}`);
991
+ return {
992
+ content: [{ type: "text", text: `Tags added to ${contentType}/${id}: ${tags.join(", ")}` }],
993
+ };
994
+ }
995
+ case "search-speakers": {
996
+ const nameQuery = args.name;
997
+ if (!nameQuery) {
998
+ return { content: [{ type: "text", text: "Error: name is required" }] };
999
+ }
1000
+ const response = await fetchAPI(`/speakers/search?name=${encodeURIComponent(nameQuery)}`);
1001
+ if (!response.ok)
1002
+ throw new Error(`HTTP error: ${response.status}`);
1003
+ const speakers = await response.json();
1004
+ if (!Array.isArray(speakers) || speakers.length === 0) {
1005
+ return { content: [{ type: "text", text: "No speakers found." }] };
1006
+ }
1007
+ const formatted = speakers.map((s) => `#${s.id} ${s.name}${s.metadata ? ` — ${s.metadata}` : ""}`);
1008
+ return {
1009
+ content: [{ type: "text", text: `Speakers:\n${formatted.join("\n")}` }],
1010
+ };
1011
+ }
1012
+ case "list-unnamed-speakers": {
1013
+ const limit = args.limit || 10;
1014
+ const offset = args.offset || 0;
1015
+ const response = await fetchAPI(`/speakers/unnamed?limit=${limit}&offset=${offset}`);
1016
+ if (!response.ok)
1017
+ throw new Error(`HTTP error: ${response.status}`);
1018
+ const speakers = await response.json();
1019
+ if (!Array.isArray(speakers) || speakers.length === 0) {
1020
+ return { content: [{ type: "text", text: "No unnamed speakers found." }] };
1021
+ }
1022
+ const formatted = speakers.map((s) => `#${s.id} ${s.name}`);
1023
+ return {
1024
+ content: [{ type: "text", text: `Unnamed speakers:\n${formatted.join("\n")}` }],
1025
+ };
1026
+ }
1027
+ case "update-speaker": {
1028
+ const speakerId = args.id;
1029
+ if (!speakerId) {
1030
+ return { content: [{ type: "text", text: "Error: id is required" }] };
1031
+ }
1032
+ const body = { id: speakerId };
1033
+ if (args.name !== undefined)
1034
+ body.name = args.name;
1035
+ if (args.metadata !== undefined)
1036
+ body.metadata = args.metadata;
1037
+ const response = await fetchAPI("/speakers/update", {
1038
+ method: "POST",
1039
+ body: JSON.stringify(body),
1040
+ });
1041
+ if (!response.ok)
1042
+ throw new Error(`HTTP error: ${response.status}`);
1043
+ return {
1044
+ content: [{ type: "text", text: `Speaker ${speakerId} updated.` }],
1045
+ };
1046
+ }
1047
+ case "merge-speakers": {
1048
+ const keepId = args.speaker_to_keep;
1049
+ const mergeId = args.speaker_to_merge;
1050
+ if (!keepId || !mergeId) {
1051
+ return { content: [{ type: "text", text: "Error: speaker_to_keep and speaker_to_merge are required" }] };
1052
+ }
1053
+ const response = await fetchAPI("/speakers/merge", {
1054
+ method: "POST",
1055
+ body: JSON.stringify({ speaker_to_keep: keepId, speaker_to_merge: mergeId }),
1056
+ });
1057
+ if (!response.ok)
1058
+ throw new Error(`HTTP error: ${response.status}`);
1059
+ return {
1060
+ content: [{ type: "text", text: `Merged speaker ${mergeId} into ${keepId}.` }],
1061
+ };
1062
+ }
1063
+ case "start-meeting": {
1064
+ const body = {};
1065
+ if (args.app)
1066
+ body.app = args.app;
1067
+ if (args.title)
1068
+ body.title = args.title;
1069
+ if (args.attendees)
1070
+ body.attendees = args.attendees;
1071
+ const response = await fetchAPI("/meetings/start", {
1072
+ method: "POST",
1073
+ body: JSON.stringify(body),
1074
+ });
1075
+ if (!response.ok)
1076
+ throw new Error(`HTTP error: ${response.status}`);
1077
+ const meeting = await response.json();
1078
+ return {
1079
+ content: [{ type: "text", text: `Meeting started (id: ${meeting.id || "ok"}).` }],
1080
+ };
1081
+ }
1082
+ case "stop-meeting": {
1083
+ const response = await fetchAPI("/meetings/stop", { method: "POST" });
1084
+ if (!response.ok)
1085
+ throw new Error(`HTTP error: ${response.status}`);
1086
+ return {
1087
+ content: [{ type: "text", text: "Meeting stopped." }],
1088
+ };
1089
+ }
1090
+ case "get-meeting": {
1091
+ const meetingId = args.id;
1092
+ if (!meetingId) {
1093
+ return { content: [{ type: "text", text: "Error: id is required" }] };
1094
+ }
1095
+ const response = await fetchAPI(`/meetings/${meetingId}`);
1096
+ if (!response.ok)
1097
+ throw new Error(`HTTP error: ${response.status}`);
1098
+ const meeting = await response.json();
1099
+ return {
1100
+ content: [{ type: "text", text: JSON.stringify(meeting, null, 2) }],
1101
+ };
1102
+ }
1103
+ case "keyword-search": {
1104
+ const params = new URLSearchParams();
1105
+ for (const [key, value] of Object.entries(args)) {
1106
+ if (value !== null && value !== undefined) {
1107
+ params.append(key, String(value));
1108
+ }
1109
+ }
1110
+ const response = await fetchAPI(`/search/keyword?${params.toString()}`);
1111
+ if (!response.ok)
1112
+ throw new Error(`HTTP error: ${response.status}`);
1113
+ const data = await response.json();
1114
+ const results = data.data || [];
1115
+ if (results.length === 0) {
1116
+ return { content: [{ type: "text", text: "No keyword search results found." }] };
1117
+ }
1118
+ const formatted = results.map((r) => {
1119
+ const content = r.content;
1120
+ return `[${r.type}] ${content?.app_name || "?"} | ${content?.timestamp || ""}\n${content?.text || content?.transcription || ""}`;
1121
+ });
1122
+ return {
1123
+ content: [{ type: "text", text: `Results: ${results.length}\n\n${formatted.join("\n---\n")}` }],
1124
+ };
1125
+ }
1126
+ case "get-frame-elements": {
1127
+ const frameId = args.frame_id;
1128
+ if (!frameId) {
1129
+ return { content: [{ type: "text", text: "Error: frame_id is required" }] };
1130
+ }
1131
+ const response = await fetchAPI(`/frames/${frameId}/elements`);
1132
+ if (!response.ok)
1133
+ throw new Error(`HTTP error: ${response.status}`);
1134
+ const elements = await response.json();
1135
+ if (!Array.isArray(elements) || elements.length === 0) {
1136
+ return { content: [{ type: "text", text: `No elements found for frame ${frameId}.` }] };
1137
+ }
1138
+ const formatted = elements.map((e) => {
1139
+ const indent = " ".repeat(Math.min(e.depth, 5));
1140
+ return `${indent}[${e.source}:${e.role}] ${e.text || "(no text)"}`;
1141
+ });
1142
+ return {
1143
+ content: [{ type: "text", text: `Frame ${frameId} elements (${elements.length}):\n${formatted.join("\n")}` }],
1144
+ };
1145
+ }
1146
+ case "control-recording": {
1147
+ const action = args.action;
1148
+ if (!action) {
1149
+ return { content: [{ type: "text", text: "Error: action is required" }] };
1150
+ }
1151
+ let endpoint;
1152
+ if (action === "start-audio")
1153
+ endpoint = "/audio/start";
1154
+ else if (action === "stop-audio")
1155
+ endpoint = "/audio/stop";
1156
+ else {
1157
+ return { content: [{ type: "text", text: `Error: unknown action '${action}'` }] };
1158
+ }
1159
+ const response = await fetchAPI(endpoint, { method: "POST" });
1160
+ if (!response.ok)
1161
+ throw new Error(`HTTP error: ${response.status}`);
1162
+ return {
1163
+ content: [{ type: "text", text: `Recording action '${action}' executed.` }],
1164
+ };
1165
+ }
784
1166
  default:
785
1167
  throw new Error(`Unknown tool: ${name}`);
786
1168
  }
package/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "manifest_version": "0.3",
3
3
  "name": "screenpipe",
4
4
  "display_name": "Screenpipe",
5
- "version": "0.8.4",
5
+ "version": "0.0.0-injected-from-package-json",
6
6
  "description": "Search your screen recordings and audio transcriptions with AI",
7
7
  "long_description": "Screenpipe is a 24/7 screen and audio recorder that lets you search everything you've seen or heard. This extension connects Claude to your local screenpipe instance, enabling AI-powered search through your digital memory.",
8
8
  "author": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screenpipe-mcp",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "MCP server for screenpipe - search your screen recordings and audio transcriptions",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -28,11 +28,15 @@ for (let i = 0; i < args.length; i++) {
28
28
 
29
29
  const SCREENPIPE_API = `http://localhost:${port}`;
30
30
 
31
+ // Read version from package.json (single source of truth)
32
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
33
+ const PKG_VERSION: string = require("../package.json").version;
34
+
31
35
  // Initialize server
32
36
  const server = new Server(
33
37
  {
34
38
  name: "screenpipe",
35
- version: "0.9.0",
39
+ version: PKG_VERSION,
36
40
  },
37
41
  {
38
42
  capabilities: {
@@ -238,6 +242,167 @@ const TOOLS: Tool[] = [
238
242
  required: ["title", "pipe_name"],
239
243
  },
240
244
  },
245
+ {
246
+ name: "health-check",
247
+ description:
248
+ "Check if screenpipe is running and healthy. Returns recording status, frame/audio stats, timestamps.",
249
+ annotations: { title: "Health Check", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
250
+ inputSchema: { type: "object", properties: {} },
251
+ },
252
+ {
253
+ name: "list-audio-devices",
254
+ description: "List available audio input/output devices for recording.",
255
+ annotations: { title: "List Audio Devices", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
256
+ inputSchema: { type: "object", properties: {} },
257
+ },
258
+ {
259
+ name: "list-monitors",
260
+ description: "List available monitors/screens for capture.",
261
+ annotations: { title: "List Monitors", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
262
+ inputSchema: { type: "object", properties: {} },
263
+ },
264
+ {
265
+ name: "add-tags",
266
+ description:
267
+ "Add tags to a content item (vision frame or audio chunk) for organization and retrieval.",
268
+ annotations: { title: "Add Tags", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
269
+ inputSchema: {
270
+ type: "object",
271
+ properties: {
272
+ content_type: { type: "string", enum: ["vision", "audio"], description: "Type of content to tag" },
273
+ id: { type: "integer", description: "Content item ID" },
274
+ tags: { type: "array", items: { type: "string" }, description: "Tags to add" },
275
+ },
276
+ required: ["content_type", "id", "tags"],
277
+ },
278
+ },
279
+ {
280
+ name: "search-speakers",
281
+ description: "Search for speakers by name prefix. Returns speaker ID, name, and metadata.",
282
+ annotations: { title: "Search Speakers", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
283
+ inputSchema: {
284
+ type: "object",
285
+ properties: {
286
+ name: { type: "string", description: "Speaker name prefix to search for (case-insensitive)" },
287
+ },
288
+ },
289
+ },
290
+ {
291
+ name: "list-unnamed-speakers",
292
+ description: "List speakers that haven't been named yet. Useful for speaker identification workflow.",
293
+ annotations: { title: "List Unnamed Speakers", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
294
+ inputSchema: {
295
+ type: "object",
296
+ properties: {
297
+ limit: { type: "integer", description: "Max results (default 10)", default: 10 },
298
+ offset: { type: "integer", description: "Pagination offset", default: 0 },
299
+ },
300
+ },
301
+ },
302
+ {
303
+ name: "update-speaker",
304
+ description: "Rename a speaker or update their metadata.",
305
+ annotations: { title: "Update Speaker", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
306
+ inputSchema: {
307
+ type: "object",
308
+ properties: {
309
+ id: { type: "integer", description: "Speaker ID" },
310
+ name: { type: "string", description: "New speaker name" },
311
+ metadata: { type: "string", description: "JSON metadata string" },
312
+ },
313
+ required: ["id"],
314
+ },
315
+ },
316
+ {
317
+ name: "merge-speakers",
318
+ description: "Merge two speakers into one (e.g. when the same person was detected as different speakers).",
319
+ annotations: { title: "Merge Speakers", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
320
+ inputSchema: {
321
+ type: "object",
322
+ properties: {
323
+ speaker_to_keep: { type: "integer", description: "Speaker ID to keep" },
324
+ speaker_to_merge: { type: "integer", description: "Speaker ID to merge into the kept one" },
325
+ },
326
+ required: ["speaker_to_keep", "speaker_to_merge"],
327
+ },
328
+ },
329
+ {
330
+ name: "start-meeting",
331
+ description: "Manually start a meeting recording session.",
332
+ annotations: { title: "Start Meeting", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
333
+ inputSchema: {
334
+ type: "object",
335
+ properties: {
336
+ app: { type: "string", description: "App name (default 'manual')", default: "manual" },
337
+ title: { type: "string", description: "Meeting title" },
338
+ attendees: { type: "string", description: "Comma-separated attendee names" },
339
+ },
340
+ },
341
+ },
342
+ {
343
+ name: "stop-meeting",
344
+ description: "Stop the current manual meeting recording session.",
345
+ annotations: { title: "Stop Meeting", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
346
+ inputSchema: { type: "object", properties: {} },
347
+ },
348
+ {
349
+ name: "get-meeting",
350
+ description: "Get details of a specific meeting by ID, including transcription and attendees.",
351
+ annotations: { title: "Get Meeting", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
352
+ inputSchema: {
353
+ type: "object",
354
+ properties: {
355
+ id: { type: "integer", description: "Meeting ID" },
356
+ },
357
+ required: ["id"],
358
+ },
359
+ },
360
+ {
361
+ name: "keyword-search",
362
+ description:
363
+ "Fast keyword search using FTS index. Faster than search-content for exact keyword matching. " +
364
+ "Returns frame IDs and matched text.",
365
+ annotations: { title: "Keyword Search", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
366
+ inputSchema: {
367
+ type: "object",
368
+ properties: {
369
+ q: { type: "string", description: "Keyword search query" },
370
+ content_type: { type: "string", enum: ["ocr", "audio", "all"], description: "Content type filter", default: "all" },
371
+ start_time: { type: "string", description: "ISO 8601 UTC or relative" },
372
+ end_time: { type: "string", description: "ISO 8601 UTC or relative" },
373
+ app_name: { type: "string", description: "Filter by app name" },
374
+ limit: { type: "integer", description: "Max results (default 20)", default: 20 },
375
+ offset: { type: "integer", description: "Pagination offset", default: 0 },
376
+ },
377
+ required: ["q"],
378
+ },
379
+ },
380
+ {
381
+ name: "get-frame-elements",
382
+ description:
383
+ "Get all UI elements for a specific frame. More targeted than search-elements when you already have a frame_id.",
384
+ annotations: { title: "Get Frame Elements", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
385
+ inputSchema: {
386
+ type: "object",
387
+ properties: {
388
+ frame_id: { type: "integer", description: "Frame ID" },
389
+ },
390
+ required: ["frame_id"],
391
+ },
392
+ },
393
+ {
394
+ name: "control-recording",
395
+ description:
396
+ "Start or stop audio/screen recording. Use to pause/resume capture.",
397
+ annotations: { title: "Control Recording", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
398
+ inputSchema: {
399
+ type: "object",
400
+ properties: {
401
+ action: { type: "string", enum: ["start-audio", "stop-audio"], description: "Recording action" },
402
+ },
403
+ required: ["action"],
404
+ },
405
+ },
241
406
  ];
242
407
 
243
408
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -868,6 +1033,235 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
868
1033
  };
869
1034
  }
870
1035
 
1036
+ case "health-check": {
1037
+ const response = await fetchAPI("/health");
1038
+ if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
1039
+ const data = await response.json();
1040
+ return {
1041
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
1042
+ };
1043
+ }
1044
+
1045
+ case "list-audio-devices": {
1046
+ const response = await fetchAPI("/audio/list");
1047
+ if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
1048
+ const devices = await response.json();
1049
+ if (!Array.isArray(devices) || devices.length === 0) {
1050
+ return { content: [{ type: "text", text: "No audio devices found." }] };
1051
+ }
1052
+ const formatted = devices.map(
1053
+ (d: { name: string; is_default: boolean; device_type?: string }) =>
1054
+ `${d.is_default ? "* " : " "}${d.name}${d.device_type ? ` (${d.device_type})` : ""}`
1055
+ );
1056
+ return {
1057
+ content: [{ type: "text", text: `Audio devices:\n${formatted.join("\n")}` }],
1058
+ };
1059
+ }
1060
+
1061
+ case "list-monitors": {
1062
+ const response = await fetchAPI("/vision/list");
1063
+ if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
1064
+ const monitors = await response.json();
1065
+ if (!Array.isArray(monitors) || monitors.length === 0) {
1066
+ return { content: [{ type: "text", text: "No monitors found." }] };
1067
+ }
1068
+ const formatted = monitors.map(
1069
+ (m: { id: number; name?: string; width?: number; height?: number; is_default?: boolean }) =>
1070
+ `${m.is_default ? "* " : " "}Monitor ${m.id}${m.name ? `: ${m.name}` : ""}${m.width ? ` (${m.width}x${m.height})` : ""}`
1071
+ );
1072
+ return {
1073
+ content: [{ type: "text", text: `Monitors:\n${formatted.join("\n")}` }],
1074
+ };
1075
+ }
1076
+
1077
+ case "add-tags": {
1078
+ const contentType = args.content_type as string;
1079
+ const id = args.id as number;
1080
+ const tags = args.tags as string[];
1081
+ if (!contentType || !id || !tags) {
1082
+ return { content: [{ type: "text", text: "Error: content_type, id, and tags are required" }] };
1083
+ }
1084
+ const response = await fetchAPI(`/tags/${contentType}/${id}`, {
1085
+ method: "POST",
1086
+ body: JSON.stringify({ tags }),
1087
+ });
1088
+ if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
1089
+ return {
1090
+ content: [{ type: "text", text: `Tags added to ${contentType}/${id}: ${tags.join(", ")}` }],
1091
+ };
1092
+ }
1093
+
1094
+ case "search-speakers": {
1095
+ const nameQuery = args.name as string;
1096
+ if (!nameQuery) {
1097
+ return { content: [{ type: "text", text: "Error: name is required" }] };
1098
+ }
1099
+ const response = await fetchAPI(`/speakers/search?name=${encodeURIComponent(nameQuery)}`);
1100
+ if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
1101
+ const speakers = await response.json();
1102
+ if (!Array.isArray(speakers) || speakers.length === 0) {
1103
+ return { content: [{ type: "text", text: "No speakers found." }] };
1104
+ }
1105
+ const formatted = speakers.map(
1106
+ (s: { id: number; name: string; metadata?: string }) =>
1107
+ `#${s.id} ${s.name}${s.metadata ? ` — ${s.metadata}` : ""}`
1108
+ );
1109
+ return {
1110
+ content: [{ type: "text", text: `Speakers:\n${formatted.join("\n")}` }],
1111
+ };
1112
+ }
1113
+
1114
+ case "list-unnamed-speakers": {
1115
+ const limit = (args.limit as number) || 10;
1116
+ const offset = (args.offset as number) || 0;
1117
+ const response = await fetchAPI(`/speakers/unnamed?limit=${limit}&offset=${offset}`);
1118
+ if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
1119
+ const speakers = await response.json();
1120
+ if (!Array.isArray(speakers) || speakers.length === 0) {
1121
+ return { content: [{ type: "text", text: "No unnamed speakers found." }] };
1122
+ }
1123
+ const formatted = speakers.map(
1124
+ (s: { id: number; name: string }) => `#${s.id} ${s.name}`
1125
+ );
1126
+ return {
1127
+ content: [{ type: "text", text: `Unnamed speakers:\n${formatted.join("\n")}` }],
1128
+ };
1129
+ }
1130
+
1131
+ case "update-speaker": {
1132
+ const speakerId = args.id as number;
1133
+ if (!speakerId) {
1134
+ return { content: [{ type: "text", text: "Error: id is required" }] };
1135
+ }
1136
+ const body: Record<string, unknown> = { id: speakerId };
1137
+ if (args.name !== undefined) body.name = args.name;
1138
+ if (args.metadata !== undefined) body.metadata = args.metadata;
1139
+ const response = await fetchAPI("/speakers/update", {
1140
+ method: "POST",
1141
+ body: JSON.stringify(body),
1142
+ });
1143
+ if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
1144
+ return {
1145
+ content: [{ type: "text", text: `Speaker ${speakerId} updated.` }],
1146
+ };
1147
+ }
1148
+
1149
+ case "merge-speakers": {
1150
+ const keepId = args.speaker_to_keep as number;
1151
+ const mergeId = args.speaker_to_merge as number;
1152
+ if (!keepId || !mergeId) {
1153
+ return { content: [{ type: "text", text: "Error: speaker_to_keep and speaker_to_merge are required" }] };
1154
+ }
1155
+ const response = await fetchAPI("/speakers/merge", {
1156
+ method: "POST",
1157
+ body: JSON.stringify({ speaker_to_keep: keepId, speaker_to_merge: mergeId }),
1158
+ });
1159
+ if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
1160
+ return {
1161
+ content: [{ type: "text", text: `Merged speaker ${mergeId} into ${keepId}.` }],
1162
+ };
1163
+ }
1164
+
1165
+ case "start-meeting": {
1166
+ const body: Record<string, unknown> = {};
1167
+ if (args.app) body.app = args.app;
1168
+ if (args.title) body.title = args.title;
1169
+ if (args.attendees) body.attendees = args.attendees;
1170
+ const response = await fetchAPI("/meetings/start", {
1171
+ method: "POST",
1172
+ body: JSON.stringify(body),
1173
+ });
1174
+ if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
1175
+ const meeting = await response.json();
1176
+ return {
1177
+ content: [{ type: "text", text: `Meeting started (id: ${meeting.id || "ok"}).` }],
1178
+ };
1179
+ }
1180
+
1181
+ case "stop-meeting": {
1182
+ const response = await fetchAPI("/meetings/stop", { method: "POST" });
1183
+ if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
1184
+ return {
1185
+ content: [{ type: "text", text: "Meeting stopped." }],
1186
+ };
1187
+ }
1188
+
1189
+ case "get-meeting": {
1190
+ const meetingId = args.id as number;
1191
+ if (!meetingId) {
1192
+ return { content: [{ type: "text", text: "Error: id is required" }] };
1193
+ }
1194
+ const response = await fetchAPI(`/meetings/${meetingId}`);
1195
+ if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
1196
+ const meeting = await response.json();
1197
+ return {
1198
+ content: [{ type: "text", text: JSON.stringify(meeting, null, 2) }],
1199
+ };
1200
+ }
1201
+
1202
+ case "keyword-search": {
1203
+ const params = new URLSearchParams();
1204
+ for (const [key, value] of Object.entries(args)) {
1205
+ if (value !== null && value !== undefined) {
1206
+ params.append(key, String(value));
1207
+ }
1208
+ }
1209
+ const response = await fetchAPI(`/search/keyword?${params.toString()}`);
1210
+ if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
1211
+ const data = await response.json();
1212
+ const results = data.data || [];
1213
+ if (results.length === 0) {
1214
+ return { content: [{ type: "text", text: "No keyword search results found." }] };
1215
+ }
1216
+ const formatted = results.map((r: Record<string, unknown>) => {
1217
+ const content = r.content as Record<string, unknown> | undefined;
1218
+ return `[${r.type}] ${content?.app_name || "?"} | ${content?.timestamp || ""}\n${content?.text || content?.transcription || ""}`;
1219
+ });
1220
+ return {
1221
+ content: [{ type: "text", text: `Results: ${results.length}\n\n${formatted.join("\n---\n")}` }],
1222
+ };
1223
+ }
1224
+
1225
+ case "get-frame-elements": {
1226
+ const frameId = args.frame_id as number;
1227
+ if (!frameId) {
1228
+ return { content: [{ type: "text", text: "Error: frame_id is required" }] };
1229
+ }
1230
+ const response = await fetchAPI(`/frames/${frameId}/elements`);
1231
+ if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
1232
+ const elements = await response.json();
1233
+ if (!Array.isArray(elements) || elements.length === 0) {
1234
+ return { content: [{ type: "text", text: `No elements found for frame ${frameId}.` }] };
1235
+ }
1236
+ const formatted = elements.map(
1237
+ (e: { role: string; text: string | null; depth: number; source: string }) => {
1238
+ const indent = " ".repeat(Math.min(e.depth, 5));
1239
+ return `${indent}[${e.source}:${e.role}] ${e.text || "(no text)"}`;
1240
+ }
1241
+ );
1242
+ return {
1243
+ content: [{ type: "text", text: `Frame ${frameId} elements (${elements.length}):\n${formatted.join("\n")}` }],
1244
+ };
1245
+ }
1246
+
1247
+ case "control-recording": {
1248
+ const action = args.action as string;
1249
+ if (!action) {
1250
+ return { content: [{ type: "text", text: "Error: action is required" }] };
1251
+ }
1252
+ let endpoint: string;
1253
+ if (action === "start-audio") endpoint = "/audio/start";
1254
+ else if (action === "stop-audio") endpoint = "/audio/stop";
1255
+ else {
1256
+ return { content: [{ type: "text", text: `Error: unknown action '${action}'` }] };
1257
+ }
1258
+ const response = await fetchAPI(endpoint, { method: "POST" });
1259
+ if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
1260
+ return {
1261
+ content: [{ type: "text", text: `Recording action '${action}' executed.` }],
1262
+ };
1263
+ }
1264
+
871
1265
  default:
872
1266
  throw new Error(`Unknown tool: ${name}`);
873
1267
  }