screenpipe-mcp 0.16.3 → 0.18.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/README.md +65 -8
- package/bun.lock +18 -7
- package/dist/http-server.d.ts +57 -1
- package/dist/http-server.js +204 -79
- package/dist/http-server.test.d.ts +1 -0
- package/dist/http-server.test.js +111 -0
- package/dist/index.js +64 -7
- package/manifest.json +6 -2
- package/package.json +1 -1
- package/src/http-server.test.ts +139 -0
- package/src/http-server.ts +240 -91
- package/src/index.ts +65 -7
- package/screenpipe-mcp.mcpb +0 -0
package/src/http-server.ts
CHANGED
|
@@ -7,10 +7,18 @@
|
|
|
7
7
|
* HTTP Server for Screenpipe MCP
|
|
8
8
|
*
|
|
9
9
|
* This allows web apps to call MCP tools over HTTP instead of stdio.
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* Run on localhost (default):
|
|
12
|
+
* npx ts-node src/http-server.ts --port 3031
|
|
13
|
+
*
|
|
14
|
+
* Expose to your LAN (requires --api-key):
|
|
15
|
+
* npx ts-node src/http-server.ts --listen-on-lan --api-key <secret>
|
|
16
|
+
*
|
|
17
|
+
* Loopback callers are always allowed without auth. Non-loopback callers
|
|
18
|
+
* must send `Authorization: Bearer <secret>` whenever --api-key is set.
|
|
11
19
|
*/
|
|
12
20
|
|
|
13
|
-
import { createServer } from "http";
|
|
21
|
+
import { createServer, type IncomingMessage, type ServerResponse } from "http";
|
|
14
22
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
23
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
16
24
|
import {
|
|
@@ -18,23 +26,121 @@ import {
|
|
|
18
26
|
ListToolsRequestSchema,
|
|
19
27
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
20
28
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
// ── CLI parsing ─────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export interface CliConfig {
|
|
32
|
+
mcpPort: number;
|
|
33
|
+
screenpipePort: number;
|
|
34
|
+
/** Bind address: "127.0.0.1" (default) or "0.0.0.0" when --listen-on-lan. */
|
|
35
|
+
host: string;
|
|
36
|
+
/** Required bearer token for non-loopback requests. Loopback skips auth. */
|
|
37
|
+
apiKey?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class CliError extends Error {}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse CLI args. Pure for testability.
|
|
44
|
+
*
|
|
45
|
+
* Mirrors the screenpipe-engine CLI: --listen-on-lan flips bind to 0.0.0.0
|
|
46
|
+
* and *requires* --api-key so we never accidentally expose an unauthenticated
|
|
47
|
+
* MCP endpoint on the user's network.
|
|
48
|
+
*/
|
|
49
|
+
export function parseArgs(argv: string[]): CliConfig {
|
|
50
|
+
let mcpPort = 3031;
|
|
51
|
+
let screenpipePort = 3030;
|
|
52
|
+
let listenOnLan = false;
|
|
53
|
+
let apiKey: string | undefined;
|
|
25
54
|
|
|
26
|
-
for (let i = 0; i <
|
|
27
|
-
|
|
28
|
-
|
|
55
|
+
for (let i = 0; i < argv.length; i++) {
|
|
56
|
+
const a = argv[i];
|
|
57
|
+
if (a === "--port" && argv[i + 1]) {
|
|
58
|
+
mcpPort = parseInt(argv[++i], 10);
|
|
59
|
+
} else if (a === "--screenpipe-port" && argv[i + 1]) {
|
|
60
|
+
screenpipePort = parseInt(argv[++i], 10);
|
|
61
|
+
} else if (a === "--listen-on-lan") {
|
|
62
|
+
listenOnLan = true;
|
|
63
|
+
} else if (a === "--api-key" && argv[i + 1]) {
|
|
64
|
+
apiKey = argv[++i];
|
|
65
|
+
} else if (a === "--help" || a === "-h") {
|
|
66
|
+
throw new CliError(usage());
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (Number.isNaN(mcpPort) || mcpPort <= 0 || mcpPort > 65535) {
|
|
71
|
+
throw new CliError(`invalid --port: ${mcpPort}`);
|
|
72
|
+
}
|
|
73
|
+
if (Number.isNaN(screenpipePort) || screenpipePort <= 0 || screenpipePort > 65535) {
|
|
74
|
+
throw new CliError(`invalid --screenpipe-port: ${screenpipePort}`);
|
|
29
75
|
}
|
|
30
|
-
if (
|
|
31
|
-
|
|
76
|
+
if (listenOnLan && !apiKey) {
|
|
77
|
+
throw new CliError(
|
|
78
|
+
"--listen-on-lan requires --api-key <secret> — refusing to expose " +
|
|
79
|
+
"an unauthenticated MCP endpoint on your network."
|
|
80
|
+
);
|
|
32
81
|
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
mcpPort,
|
|
85
|
+
screenpipePort,
|
|
86
|
+
host: listenOnLan ? "0.0.0.0" : "127.0.0.1",
|
|
87
|
+
apiKey,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function usage(): string {
|
|
92
|
+
return [
|
|
93
|
+
"screenpipe-mcp http server",
|
|
94
|
+
"",
|
|
95
|
+
" --port <n> listen port (default 3031)",
|
|
96
|
+
" --screenpipe-port <n> upstream screenpipe API port (default 3030)",
|
|
97
|
+
" --listen-on-lan bind 0.0.0.0 instead of 127.0.0.1",
|
|
98
|
+
" (requires --api-key)",
|
|
99
|
+
" --api-key <secret> bearer token for non-loopback requests",
|
|
100
|
+
" --help, -h show this message",
|
|
101
|
+
].join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Auth ────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* True if `req` came from the local machine. Covers IPv4 loopback,
|
|
108
|
+
* IPv6 loopback, and IPv4-mapped-IPv6 loopback (`::ffff:127.x`).
|
|
109
|
+
*/
|
|
110
|
+
export function isLoopbackRequest(req: { socket: { remoteAddress?: string } }): boolean {
|
|
111
|
+
const addr = req.socket.remoteAddress ?? "";
|
|
112
|
+
if (addr === "127.0.0.1" || addr === "::1") return true;
|
|
113
|
+
if (addr.startsWith("::ffff:127.")) return true;
|
|
114
|
+
return false;
|
|
33
115
|
}
|
|
34
116
|
|
|
35
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Authorization decision. Loopback is always allowed; non-loopback requires
|
|
119
|
+
* a matching bearer token when one is configured. If no api key is set
|
|
120
|
+
* (loopback-only deployment), non-loopback shouldn't even be reachable —
|
|
121
|
+
* but we still 401 it as belt-and-suspenders.
|
|
122
|
+
*/
|
|
123
|
+
export function isAuthorized(
|
|
124
|
+
req: { socket: { remoteAddress?: string }; headers: { authorization?: string } },
|
|
125
|
+
apiKey: string | undefined
|
|
126
|
+
): boolean {
|
|
127
|
+
if (isLoopbackRequest(req)) return true;
|
|
128
|
+
if (!apiKey) return false;
|
|
129
|
+
const expected = `Bearer ${apiKey}`;
|
|
130
|
+
const got = req.headers.authorization ?? "";
|
|
131
|
+
return constantTimeEq(got, expected);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Constant-time string compare to keep timing attacks off the table. */
|
|
135
|
+
function constantTimeEq(a: string, b: string): boolean {
|
|
136
|
+
if (a.length !== b.length) return false;
|
|
137
|
+
let diff = 0;
|
|
138
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
139
|
+
return diff === 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Tool definitions ────────────────────────────────────────────────────
|
|
36
143
|
|
|
37
|
-
// Tool definitions
|
|
38
144
|
const TOOLS = [
|
|
39
145
|
{
|
|
40
146
|
name: "search_content",
|
|
@@ -52,17 +158,12 @@ const TOOLS = [
|
|
|
52
158
|
content_type: {
|
|
53
159
|
type: "string",
|
|
54
160
|
enum: ["all", "ocr", "audio", "input", "accessibility"],
|
|
55
|
-
description:
|
|
161
|
+
description:
|
|
162
|
+
"Content type filter: 'ocr' (screen text), 'audio' (transcriptions), 'input' (clicks, keystrokes, clipboard, app switches), 'accessibility' (accessibility tree text), 'all'. Default: 'all'",
|
|
56
163
|
default: "all",
|
|
57
164
|
},
|
|
58
|
-
limit: {
|
|
59
|
-
|
|
60
|
-
description: "Max results. Default: 10",
|
|
61
|
-
},
|
|
62
|
-
offset: {
|
|
63
|
-
type: "integer",
|
|
64
|
-
description: "Skip N results for pagination. Default: 0",
|
|
65
|
-
},
|
|
165
|
+
limit: { type: "integer", description: "Max results. Default: 10" },
|
|
166
|
+
offset: { type: "integer", description: "Skip N results for pagination. Default: 0" },
|
|
66
167
|
start_time: {
|
|
67
168
|
type: "string",
|
|
68
169
|
description: "ISO 8601 UTC start time (e.g., 2024-01-15T10:00:00Z)",
|
|
@@ -75,29 +176,27 @@ const TOOLS = [
|
|
|
75
176
|
type: "string",
|
|
76
177
|
description: "Filter by app (e.g., 'Google Chrome', 'Slack', 'zoom.us')",
|
|
77
178
|
},
|
|
78
|
-
window_name: {
|
|
79
|
-
type: "string",
|
|
80
|
-
description: "Filter by window title",
|
|
81
|
-
},
|
|
179
|
+
window_name: { type: "string", description: "Filter by window title" },
|
|
82
180
|
},
|
|
83
181
|
},
|
|
84
182
|
},
|
|
85
183
|
];
|
|
86
184
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
...options.headers,
|
|
95
|
-
}
|
|
96
|
-
});
|
|
185
|
+
// ── Tool handlers ───────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
function makeFetchAPI(screenpipePort: number) {
|
|
188
|
+
const base = `http://localhost:${screenpipePort}`;
|
|
189
|
+
return async (endpoint: string, options: RequestInit = {}): Promise<Response> =>
|
|
190
|
+
fetch(`${base}${endpoint}`, {
|
|
191
|
+
...options,
|
|
192
|
+
headers: { "Content-Type": "application/json", ...options.headers },
|
|
193
|
+
});
|
|
97
194
|
}
|
|
98
195
|
|
|
99
|
-
|
|
100
|
-
|
|
196
|
+
async function handleSearchContent(
|
|
197
|
+
fetchAPI: ReturnType<typeof makeFetchAPI>,
|
|
198
|
+
args: Record<string, unknown>
|
|
199
|
+
) {
|
|
101
200
|
const params = new URLSearchParams();
|
|
102
201
|
for (const [key, value] of Object.entries(args)) {
|
|
103
202
|
if (value !== null && value !== undefined) {
|
|
@@ -133,26 +232,29 @@ async function handleSearchContent(args: Record<string, unknown>) {
|
|
|
133
232
|
if (result.type === "OCR") {
|
|
134
233
|
formattedResults.push(
|
|
135
234
|
`[OCR] ${content.app_name || "?"} | ${content.window_name || "?"}\n` +
|
|
136
|
-
|
|
137
|
-
|
|
235
|
+
`${content.timestamp || ""}\n` +
|
|
236
|
+
`${content.text || ""}`
|
|
138
237
|
);
|
|
139
238
|
} else if (result.type === "Audio") {
|
|
140
239
|
formattedResults.push(
|
|
141
240
|
`[Audio] ${content.device_name || "?"}\n` +
|
|
142
|
-
|
|
143
|
-
|
|
241
|
+
`${content.timestamp || ""}\n` +
|
|
242
|
+
`${content.transcription || ""}`
|
|
144
243
|
);
|
|
145
244
|
} else if (result.type === "UI" || result.type === "Accessibility") {
|
|
146
245
|
formattedResults.push(
|
|
147
246
|
`[Accessibility] ${content.app_name || "?"} | ${content.window_name || "?"}\n` +
|
|
148
|
-
|
|
149
|
-
|
|
247
|
+
`${content.timestamp || ""}\n` +
|
|
248
|
+
`${content.text || ""}`
|
|
150
249
|
);
|
|
151
250
|
}
|
|
152
251
|
}
|
|
153
252
|
|
|
154
|
-
const header =
|
|
155
|
-
|
|
253
|
+
const header =
|
|
254
|
+
`Results: ${results.length}/${pagination.total || "?"}` +
|
|
255
|
+
(pagination.total > results.length
|
|
256
|
+
? ` (use offset=${(pagination.offset || 0) + results.length} for more)`
|
|
257
|
+
: "");
|
|
156
258
|
|
|
157
259
|
return {
|
|
158
260
|
content: [
|
|
@@ -164,11 +266,12 @@ async function handleSearchContent(args: Record<string, unknown>) {
|
|
|
164
266
|
};
|
|
165
267
|
}
|
|
166
268
|
|
|
167
|
-
//
|
|
269
|
+
// ── MCP server factory ──────────────────────────────────────────────────
|
|
270
|
+
|
|
168
271
|
// Each HTTP session gets its own Server — the MCP SDK requires a 1:1
|
|
169
272
|
// mapping between Server and transport (reusing a Server across
|
|
170
273
|
// transports throws "Already connected to a transport").
|
|
171
|
-
function createMcpServer(): Server {
|
|
274
|
+
function createMcpServer(fetchAPI: ReturnType<typeof makeFetchAPI>): Server {
|
|
172
275
|
const s = new Server(
|
|
173
276
|
{ name: "screenpipe-http", version: "0.14.0" },
|
|
174
277
|
{ capabilities: { tools: {} } }
|
|
@@ -179,65 +282,111 @@ function createMcpServer(): Server {
|
|
|
179
282
|
s.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
180
283
|
const { name, arguments: args } = request.params;
|
|
181
284
|
if (!args) throw new Error("Missing arguments");
|
|
182
|
-
if (name === "search_content") return handleSearchContent(args);
|
|
285
|
+
if (name === "search_content") return handleSearchContent(fetchAPI, args);
|
|
183
286
|
throw new Error(`Unknown tool: ${name}`);
|
|
184
287
|
});
|
|
185
288
|
|
|
186
289
|
return s;
|
|
187
290
|
}
|
|
188
291
|
|
|
189
|
-
//
|
|
190
|
-
const sessions = new Map<string, { server: Server; transport: StreamableHTTPServerTransport }>();
|
|
292
|
+
// ── HTTP server ─────────────────────────────────────────────────────────
|
|
191
293
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
294
|
+
export function buildHttpServer(config: CliConfig) {
|
|
295
|
+
const fetchAPI = makeFetchAPI(config.screenpipePort);
|
|
296
|
+
const sessions = new Map<
|
|
297
|
+
string,
|
|
298
|
+
{ server: Server; transport: StreamableHTTPServerTransport }
|
|
299
|
+
>();
|
|
197
300
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
res.
|
|
201
|
-
|
|
202
|
-
|
|
301
|
+
return createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
302
|
+
// CORS
|
|
303
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
304
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
305
|
+
res.setHeader(
|
|
306
|
+
"Access-Control-Allow-Headers",
|
|
307
|
+
"Content-Type, Authorization, mcp-session-id"
|
|
308
|
+
);
|
|
203
309
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
310
|
+
if (req.method === "OPTIONS") {
|
|
311
|
+
res.writeHead(204);
|
|
312
|
+
res.end();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
210
315
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
316
|
+
// Health check is unauthenticated — monitors / load balancers need it.
|
|
317
|
+
// It only reveals session count, no user data.
|
|
318
|
+
if (req.url === "/health") {
|
|
319
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
320
|
+
res.end(JSON.stringify({ status: "ok", sessions: sessions.size }));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
214
323
|
|
|
215
|
-
|
|
324
|
+
// Auth gate for everything else.
|
|
325
|
+
if (!isAuthorized(req, config.apiKey)) {
|
|
326
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
327
|
+
res.end(JSON.stringify({ error: "unauthorized" }));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// MCP endpoint
|
|
332
|
+
if (req.url === "/mcp" || req.url?.startsWith("/mcp?")) {
|
|
333
|
+
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
334
|
+
let session = sessionId ? sessions.get(sessionId) : undefined;
|
|
216
335
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
336
|
+
if (!session) {
|
|
337
|
+
const server = createMcpServer(fetchAPI);
|
|
338
|
+
const transport = new StreamableHTTPServerTransport({
|
|
339
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
340
|
+
});
|
|
222
341
|
|
|
223
|
-
|
|
342
|
+
await server.connect(transport);
|
|
224
343
|
|
|
225
|
-
|
|
226
|
-
|
|
344
|
+
if (transport.sessionId) {
|
|
345
|
+
sessions.set(transport.sessionId, { server, transport });
|
|
346
|
+
}
|
|
347
|
+
session = { server, transport };
|
|
227
348
|
}
|
|
228
|
-
|
|
349
|
+
|
|
350
|
+
await session.transport.handleRequest(req, res);
|
|
351
|
+
return;
|
|
229
352
|
}
|
|
230
353
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
354
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
355
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── Entry point ─────────────────────────────────────────────────────────
|
|
234
360
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
361
|
+
// Don't auto-start when imported (e.g. by tests). Compare to argv[1] so
|
|
362
|
+
// `node dist/http-server.js` and `npx ts-node src/http-server.ts` both
|
|
363
|
+
// match, but `import "./http-server"` from a test does not.
|
|
364
|
+
const isMain =
|
|
365
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
366
|
+
(typeof require !== "undefined" && (require as any).main === module) ||
|
|
367
|
+
process.argv[1]?.endsWith("http-server.ts") ||
|
|
368
|
+
process.argv[1]?.endsWith("http-server.js");
|
|
238
369
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
})
|
|
370
|
+
if (isMain) {
|
|
371
|
+
let config: CliConfig;
|
|
372
|
+
try {
|
|
373
|
+
config = parseArgs(process.argv.slice(2));
|
|
374
|
+
} catch (e) {
|
|
375
|
+
if (e instanceof CliError) {
|
|
376
|
+
console.error(e.message);
|
|
377
|
+
process.exit(2);
|
|
378
|
+
}
|
|
379
|
+
throw e;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const server = buildHttpServer(config);
|
|
383
|
+
server.listen(config.mcpPort, config.host, () => {
|
|
384
|
+
const printable = config.host === "0.0.0.0" ? "0.0.0.0 (LAN)" : config.host;
|
|
385
|
+
console.log(`Screenpipe MCP HTTP server listening on ${printable}:${config.mcpPort}`);
|
|
386
|
+
console.log(` MCP endpoint: http://${config.host}:${config.mcpPort}/mcp`);
|
|
387
|
+
console.log(` Health check: http://${config.host}:${config.mcpPort}/health`);
|
|
388
|
+
if (config.apiKey) {
|
|
389
|
+
console.log(" Auth required for non-loopback requests (Authorization: Bearer …)");
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -389,10 +389,10 @@ const TOOLS: Tool[] = [
|
|
|
389
389
|
inputSchema: {
|
|
390
390
|
type: "object",
|
|
391
391
|
properties: {
|
|
392
|
-
|
|
393
|
-
|
|
392
|
+
speaker_to_keep_id: { type: "integer", description: "Speaker ID to keep" },
|
|
393
|
+
speaker_to_merge_id: { type: "integer", description: "Speaker ID to merge into the kept one" },
|
|
394
394
|
},
|
|
395
|
-
required: ["
|
|
395
|
+
required: ["speaker_to_keep_id", "speaker_to_merge_id"],
|
|
396
396
|
},
|
|
397
397
|
},
|
|
398
398
|
{
|
|
@@ -426,6 +426,32 @@ const TOOLS: Tool[] = [
|
|
|
426
426
|
required: ["id"],
|
|
427
427
|
},
|
|
428
428
|
},
|
|
429
|
+
{
|
|
430
|
+
name: "update-meeting",
|
|
431
|
+
description:
|
|
432
|
+
"Update a meeting's mutable fields (title, attendees, note, app, start/end). Partial: only the fields you pass are written, " +
|
|
433
|
+
"others stay as-is. Use this to save an AI-generated summary into the meeting note — read the current note first via get-meeting " +
|
|
434
|
+
"and pass the existing notes plus your additions so you don't overwrite the user's writing. " +
|
|
435
|
+
"Convention: append AI-generated summary text under a `## Summary` heading at the bottom of the existing note.",
|
|
436
|
+
annotations: { title: "Update Meeting", readOnlyHint: false, destructiveHint: false, openWorldHint: false, idempotentHint: true },
|
|
437
|
+
inputSchema: {
|
|
438
|
+
type: "object",
|
|
439
|
+
properties: {
|
|
440
|
+
id: { type: "integer", description: "Meeting ID" },
|
|
441
|
+
title: { type: "string", description: "Meeting title" },
|
|
442
|
+
attendees: { type: "string", description: "Comma-separated attendee names" },
|
|
443
|
+
note: {
|
|
444
|
+
type: "string",
|
|
445
|
+
description:
|
|
446
|
+
"Full new note body. To preserve existing notes, fetch them first via get-meeting and concatenate before passing.",
|
|
447
|
+
},
|
|
448
|
+
meeting_app: { type: "string", description: "App / source name (e.g. 'meet.google.com', 'manual')" },
|
|
449
|
+
meeting_start: { type: "string", description: "ISO 8601 start time (rarely needed)" },
|
|
450
|
+
meeting_end: { type: "string", description: "ISO 8601 end time (rarely needed)" },
|
|
451
|
+
},
|
|
452
|
+
required: ["id"],
|
|
453
|
+
},
|
|
454
|
+
},
|
|
429
455
|
{
|
|
430
456
|
name: "keyword-search",
|
|
431
457
|
description:
|
|
@@ -1244,14 +1270,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1244
1270
|
}
|
|
1245
1271
|
|
|
1246
1272
|
case "merge-speakers": {
|
|
1247
|
-
const keepId = args.
|
|
1248
|
-
const mergeId = args.
|
|
1273
|
+
const keepId = args.speaker_to_keep_id as number;
|
|
1274
|
+
const mergeId = args.speaker_to_merge_id as number;
|
|
1249
1275
|
if (!keepId || !mergeId) {
|
|
1250
|
-
return { content: [{ type: "text", text: "Error:
|
|
1276
|
+
return { content: [{ type: "text", text: "Error: speaker_to_keep_id and speaker_to_merge_id are required" }] };
|
|
1251
1277
|
}
|
|
1252
1278
|
const response = await fetchAPI("/speakers/merge", {
|
|
1253
1279
|
method: "POST",
|
|
1254
|
-
body: JSON.stringify({
|
|
1280
|
+
body: JSON.stringify({ speaker_to_keep_id: keepId, speaker_to_merge_id: mergeId }),
|
|
1255
1281
|
});
|
|
1256
1282
|
if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
|
|
1257
1283
|
return {
|
|
@@ -1296,6 +1322,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1296
1322
|
};
|
|
1297
1323
|
}
|
|
1298
1324
|
|
|
1325
|
+
case "update-meeting": {
|
|
1326
|
+
const meetingId = args.id as number;
|
|
1327
|
+
if (!meetingId) {
|
|
1328
|
+
return { content: [{ type: "text", text: "Error: id is required" }] };
|
|
1329
|
+
}
|
|
1330
|
+
// Build partial body — only forward fields the caller provided.
|
|
1331
|
+
const body: Record<string, unknown> = {};
|
|
1332
|
+
for (const k of ["title", "attendees", "note", "meeting_app", "meeting_start", "meeting_end"] as const) {
|
|
1333
|
+
if (args[k] !== undefined && args[k] !== null) body[k] = args[k];
|
|
1334
|
+
}
|
|
1335
|
+
if (Object.keys(body).length === 0) {
|
|
1336
|
+
return {
|
|
1337
|
+
content: [
|
|
1338
|
+
{
|
|
1339
|
+
type: "text",
|
|
1340
|
+
text: "Error: pass at least one field to update (title, attendees, note, meeting_app, meeting_start, meeting_end).",
|
|
1341
|
+
},
|
|
1342
|
+
],
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
const response = await fetchAPI(`/meetings/${meetingId}`, {
|
|
1346
|
+
method: "PATCH",
|
|
1347
|
+
headers: { "Content-Type": "application/json" },
|
|
1348
|
+
body: JSON.stringify(body),
|
|
1349
|
+
});
|
|
1350
|
+
if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
|
|
1351
|
+
const updated = await response.json();
|
|
1352
|
+
return {
|
|
1353
|
+
content: [{ type: "text", text: JSON.stringify(updated, null, 2) }],
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1299
1357
|
case "keyword-search": {
|
|
1300
1358
|
const params = new URLSearchParams();
|
|
1301
1359
|
for (const [key, value] of Object.entries(args)) {
|
package/screenpipe-mcp.mcpb
DELETED
|
Binary file
|