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