screenpipe-mcp 0.17.0 → 0.18.1
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 +176 -28
- package/package.json +1 -1
- package/src/index.ts +182 -27
package/dist/index.js
CHANGED
|
@@ -53,32 +53,60 @@ for (let i = 0; i < args.length; i++) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
const SCREENPIPE_API = `http://localhost:${port}`;
|
|
56
|
-
// Discover API key
|
|
56
|
+
// Discover the local API key, in priority order:
|
|
57
|
+
//
|
|
58
|
+
// 1. env vars set by the launcher (Claude Desktop config, terminal, etc.)
|
|
59
|
+
// 2. direct sqlite3 read of ~/.screenpipe/db.sqlite (plaintext entries only —
|
|
60
|
+
// encrypted ones need keychain, handled by 3+)
|
|
61
|
+
// 3. bundled `bun` shipped with the desktop app → `bun x screenpipe@latest auth token`
|
|
62
|
+
// this is the kill-shot for Claude-Desktop-via-MCP: Claude strips PATH so
|
|
63
|
+
// `npx` and `sqlite3` lookups fail, but the desktop app's bundled bun is
|
|
64
|
+
// at a deterministic path. We invoke it with an absolute path, which
|
|
65
|
+
// then runs the screenpipe CLI's `auth token` command — which goes
|
|
66
|
+
// through `find_api_auth_key` (handles the encrypted-secret-store case).
|
|
67
|
+
// 4. node-adjacent npx (legacy fallback for users without the desktop app)
|
|
68
|
+
// 5. PATH-based npx (very last resort)
|
|
69
|
+
//
|
|
70
|
+
// If all 5 miss we log a loud stderr warning so it surfaces in the host's
|
|
71
|
+
// MCP log instead of the user just seeing 403s with no explanation.
|
|
57
72
|
function discoverApiKey() {
|
|
58
73
|
const envKey = process.env.SCREENPIPE_LOCAL_API_KEY || process.env.SCREENPIPE_API_KEY;
|
|
59
74
|
if (envKey)
|
|
60
75
|
return envKey;
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
61
77
|
const os = require("os");
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
62
79
|
const path = require("path");
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
63
81
|
const fs = require("fs");
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
64
83
|
const { execFileSync, execSync } = require("child_process");
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
84
|
+
// Common absolute paths for `sqlite3`. Claude Desktop's MCP launcher
|
|
85
|
+
// strips PATH so the bare command name `sqlite3` would fail spawn
|
|
86
|
+
// even though `/usr/bin/sqlite3` is always present on macOS. Try the
|
|
87
|
+
// bare name first (cheap; works on dev machines with a normal shell)
|
|
88
|
+
// then walk known absolute paths.
|
|
89
|
+
const sqliteCandidates = process.platform === "win32"
|
|
90
|
+
? ["sqlite3.exe", "C\\:Windows\\System32\\sqlite3.exe"]
|
|
91
|
+
: process.platform === "darwin"
|
|
92
|
+
? ["sqlite3", "/usr/bin/sqlite3", "/opt/homebrew/bin/sqlite3", "/usr/local/bin/sqlite3"]
|
|
93
|
+
: ["sqlite3", "/usr/bin/sqlite3", "/usr/local/bin/sqlite3"];
|
|
94
|
+
// 2. Direct sqlite3 read of the secret store. Only succeeds for
|
|
95
|
+
// plaintext entries (nonce all zeros). Encrypted entries fall
|
|
96
|
+
// through to the CLI path which can decrypt via keychain.
|
|
69
97
|
try {
|
|
70
98
|
const dbPath = path.join(os.homedir(), ".screenpipe", "db.sqlite");
|
|
71
99
|
if (fs.existsSync(dbPath)) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
100
|
+
let row = null;
|
|
101
|
+
for (const candidate of sqliteCandidates) {
|
|
102
|
+
try {
|
|
103
|
+
row = execFileSync(candidate, [dbPath, "SELECT hex(nonce), value FROM secrets WHERE key = 'api_auth_key';"], { timeout: 5000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// try next candidate
|
|
108
|
+
}
|
|
109
|
+
}
|
|
82
110
|
if (row) {
|
|
83
111
|
const sepIdx = row.indexOf("|");
|
|
84
112
|
const nonceHex = sepIdx >= 0 ? row.substring(0, sepIdx) : "";
|
|
@@ -91,34 +119,97 @@ function discoverApiKey() {
|
|
|
91
119
|
if (value.startsWith("sp-"))
|
|
92
120
|
return value;
|
|
93
121
|
}
|
|
94
|
-
// Non-zero nonce = encrypted — fall through to
|
|
122
|
+
// Non-zero nonce = encrypted — fall through to bun/npx which decrypt via keychain.
|
|
95
123
|
}
|
|
96
124
|
}
|
|
97
125
|
}
|
|
98
126
|
catch { }
|
|
99
|
-
//
|
|
127
|
+
// 3. Bundled `bun` shipped with the desktop app. The Tauri externalBin
|
|
128
|
+
// config (apps/screenpipe-app-tauri/src-tauri/tauri.prod.conf.json)
|
|
129
|
+
// places it next to the main app executable; on each OS the install
|
|
130
|
+
// path is deterministic so we don't need PATH or current_exe — both
|
|
131
|
+
// of which Claude Desktop's MCP launcher rolls back.
|
|
132
|
+
const home = os.homedir();
|
|
133
|
+
const bunCandidates = process.platform === "darwin"
|
|
134
|
+
? [
|
|
135
|
+
// Standard system-wide install
|
|
136
|
+
"/Applications/screenpipe.app/Contents/MacOS/bun",
|
|
137
|
+
// Per-user install
|
|
138
|
+
path.join(home, "Applications", "screenpipe.app", "Contents", "MacOS", "bun"),
|
|
139
|
+
]
|
|
140
|
+
: process.platform === "win32"
|
|
141
|
+
? [
|
|
142
|
+
// NSIS per-user (default on Windows)
|
|
143
|
+
path.join(home, "AppData", "Local", "screenpipe", "bun.exe"),
|
|
144
|
+
// Per-user under "screenpipe-app" (older builds)
|
|
145
|
+
path.join(home, "AppData", "Local", "screenpipe-app", "bun.exe"),
|
|
146
|
+
// System-wide install
|
|
147
|
+
"C:\\Program Files\\screenpipe\\bun.exe",
|
|
148
|
+
]
|
|
149
|
+
: [
|
|
150
|
+
// Linux .deb
|
|
151
|
+
"/opt/screenpipe/bun",
|
|
152
|
+
"/usr/lib/screenpipe/bun",
|
|
153
|
+
"/usr/bin/bun",
|
|
154
|
+
];
|
|
155
|
+
for (const bunPath of bunCandidates) {
|
|
156
|
+
if (!fs.existsSync(bunPath))
|
|
157
|
+
continue;
|
|
158
|
+
try {
|
|
159
|
+
const token = execFileSync(bunPath, ["x", "screenpipe@latest", "auth", "token"], {
|
|
160
|
+
timeout: 30000, // first run downloads the package; subsequent runs are cached
|
|
161
|
+
encoding: "utf-8",
|
|
162
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
163
|
+
}).trim();
|
|
164
|
+
if (token && token.startsWith("sp-"))
|
|
165
|
+
return token;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// try next candidate
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// 4. npx adjacent to the running node — works in dev environments
|
|
172
|
+
// where the user installed @screenpipe/mcp via npx without the
|
|
173
|
+
// desktop app.
|
|
100
174
|
try {
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
175
|
+
const npxName = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
176
|
+
const npxPath = path.join(path.dirname(process.execPath), npxName);
|
|
177
|
+
if (fs.existsSync(npxPath)) {
|
|
178
|
+
const token = execFileSync(npxPath, ["screenpipe@latest", "auth", "token"], {
|
|
179
|
+
timeout: 30000,
|
|
180
|
+
encoding: "utf-8",
|
|
181
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
182
|
+
}).trim();
|
|
183
|
+
if (token && token.startsWith("sp-"))
|
|
184
|
+
return token;
|
|
185
|
+
}
|
|
109
186
|
}
|
|
110
187
|
catch { }
|
|
111
|
-
//
|
|
188
|
+
// 5. PATH-based npx — last-ditch. Will fail under Claude Desktop's
|
|
189
|
+
// sanitized env; useful only on raw shells.
|
|
112
190
|
try {
|
|
113
191
|
const token = execSync("npx screenpipe@latest auth token", {
|
|
114
|
-
timeout:
|
|
192
|
+
timeout: 30000,
|
|
115
193
|
encoding: "utf-8",
|
|
116
194
|
stdio: ["pipe", "pipe", "pipe"],
|
|
117
195
|
}).trim();
|
|
118
|
-
if (token)
|
|
196
|
+
if (token && token.startsWith("sp-"))
|
|
119
197
|
return token;
|
|
120
198
|
}
|
|
121
199
|
catch { }
|
|
200
|
+
// All five paths missed. Log loudly to stderr so the host's MCP
|
|
201
|
+
// panel surfaces this instead of the user seeing cryptic 403s from
|
|
202
|
+
// the screenpipe server on every tool call.
|
|
203
|
+
process.stderr.write([
|
|
204
|
+
"[screenpipe-mcp] could not discover SCREENPIPE_LOCAL_API_KEY from any source.",
|
|
205
|
+
" - env vars (SCREENPIPE_LOCAL_API_KEY / SCREENPIPE_API_KEY) not set",
|
|
206
|
+
" - direct sqlite3 read of ~/.screenpipe/db.sqlite failed",
|
|
207
|
+
" - bundled `bun` from screenpipe.app not found at any known install path",
|
|
208
|
+
" - npx fallback unavailable",
|
|
209
|
+
"Fix: set SCREENPIPE_LOCAL_API_KEY in your MCP launcher's env block,",
|
|
210
|
+
"or install the screenpipe desktop app (https://screenpi.pe).",
|
|
211
|
+
"",
|
|
212
|
+
].join("\n"));
|
|
122
213
|
return "";
|
|
123
214
|
}
|
|
124
215
|
const API_KEY = discoverApiKey();
|
|
@@ -437,6 +528,30 @@ const TOOLS = [
|
|
|
437
528
|
required: ["id"],
|
|
438
529
|
},
|
|
439
530
|
},
|
|
531
|
+
{
|
|
532
|
+
name: "update-meeting",
|
|
533
|
+
description: "Update a meeting's mutable fields (title, attendees, note, app, start/end). Partial: only the fields you pass are written, " +
|
|
534
|
+
"others stay as-is. Use this to save an AI-generated summary into the meeting note — read the current note first via get-meeting " +
|
|
535
|
+
"and pass the existing notes plus your additions so you don't overwrite the user's writing. " +
|
|
536
|
+
"Convention: append AI-generated summary text under a `## Summary` heading at the bottom of the existing note.",
|
|
537
|
+
annotations: { title: "Update Meeting", readOnlyHint: false, destructiveHint: false, openWorldHint: false, idempotentHint: true },
|
|
538
|
+
inputSchema: {
|
|
539
|
+
type: "object",
|
|
540
|
+
properties: {
|
|
541
|
+
id: { type: "integer", description: "Meeting ID" },
|
|
542
|
+
title: { type: "string", description: "Meeting title" },
|
|
543
|
+
attendees: { type: "string", description: "Comma-separated attendee names" },
|
|
544
|
+
note: {
|
|
545
|
+
type: "string",
|
|
546
|
+
description: "Full new note body. To preserve existing notes, fetch them first via get-meeting and concatenate before passing.",
|
|
547
|
+
},
|
|
548
|
+
meeting_app: { type: "string", description: "App / source name (e.g. 'meet.google.com', 'manual')" },
|
|
549
|
+
meeting_start: { type: "string", description: "ISO 8601 start time (rarely needed)" },
|
|
550
|
+
meeting_end: { type: "string", description: "ISO 8601 end time (rarely needed)" },
|
|
551
|
+
},
|
|
552
|
+
required: ["id"],
|
|
553
|
+
},
|
|
554
|
+
},
|
|
440
555
|
{
|
|
441
556
|
name: "keyword-search",
|
|
442
557
|
description: "Fast keyword search using FTS index. Faster than search-content for exact keyword matching. " +
|
|
@@ -1186,6 +1301,39 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
1186
1301
|
content: [{ type: "text", text: JSON.stringify(meeting, null, 2) }],
|
|
1187
1302
|
};
|
|
1188
1303
|
}
|
|
1304
|
+
case "update-meeting": {
|
|
1305
|
+
const meetingId = args.id;
|
|
1306
|
+
if (!meetingId) {
|
|
1307
|
+
return { content: [{ type: "text", text: "Error: id is required" }] };
|
|
1308
|
+
}
|
|
1309
|
+
// Build partial body — only forward fields the caller provided.
|
|
1310
|
+
const body = {};
|
|
1311
|
+
for (const k of ["title", "attendees", "note", "meeting_app", "meeting_start", "meeting_end"]) {
|
|
1312
|
+
if (args[k] !== undefined && args[k] !== null)
|
|
1313
|
+
body[k] = args[k];
|
|
1314
|
+
}
|
|
1315
|
+
if (Object.keys(body).length === 0) {
|
|
1316
|
+
return {
|
|
1317
|
+
content: [
|
|
1318
|
+
{
|
|
1319
|
+
type: "text",
|
|
1320
|
+
text: "Error: pass at least one field to update (title, attendees, note, meeting_app, meeting_start, meeting_end).",
|
|
1321
|
+
},
|
|
1322
|
+
],
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
const response = await fetchAPI(`/meetings/${meetingId}`, {
|
|
1326
|
+
method: "PATCH",
|
|
1327
|
+
headers: { "Content-Type": "application/json" },
|
|
1328
|
+
body: JSON.stringify(body),
|
|
1329
|
+
});
|
|
1330
|
+
if (!response.ok)
|
|
1331
|
+
throw new Error(`HTTP error: ${response.status}`);
|
|
1332
|
+
const updated = await response.json();
|
|
1333
|
+
return {
|
|
1334
|
+
content: [{ type: "text", text: JSON.stringify(updated, null, 2) }],
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1189
1337
|
case "keyword-search": {
|
|
1190
1338
|
const params = new URLSearchParams();
|
|
1191
1339
|
for (const [key, value] of Object.entries(args)) {
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -28,33 +28,66 @@ for (let i = 0; i < args.length; i++) {
|
|
|
28
28
|
|
|
29
29
|
const SCREENPIPE_API = `http://localhost:${port}`;
|
|
30
30
|
|
|
31
|
-
// Discover API key
|
|
31
|
+
// Discover the local API key, in priority order:
|
|
32
|
+
//
|
|
33
|
+
// 1. env vars set by the launcher (Claude Desktop config, terminal, etc.)
|
|
34
|
+
// 2. direct sqlite3 read of ~/.screenpipe/db.sqlite (plaintext entries only —
|
|
35
|
+
// encrypted ones need keychain, handled by 3+)
|
|
36
|
+
// 3. bundled `bun` shipped with the desktop app → `bun x screenpipe@latest auth token`
|
|
37
|
+
// this is the kill-shot for Claude-Desktop-via-MCP: Claude strips PATH so
|
|
38
|
+
// `npx` and `sqlite3` lookups fail, but the desktop app's bundled bun is
|
|
39
|
+
// at a deterministic path. We invoke it with an absolute path, which
|
|
40
|
+
// then runs the screenpipe CLI's `auth token` command — which goes
|
|
41
|
+
// through `find_api_auth_key` (handles the encrypted-secret-store case).
|
|
42
|
+
// 4. node-adjacent npx (legacy fallback for users without the desktop app)
|
|
43
|
+
// 5. PATH-based npx (very last resort)
|
|
44
|
+
//
|
|
45
|
+
// If all 5 miss we log a loud stderr warning so it surfaces in the host's
|
|
46
|
+
// MCP log instead of the user just seeing 403s with no explanation.
|
|
32
47
|
function discoverApiKey(): string {
|
|
33
48
|
const envKey = process.env.SCREENPIPE_LOCAL_API_KEY || process.env.SCREENPIPE_API_KEY;
|
|
34
49
|
if (envKey) return envKey;
|
|
35
50
|
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
36
52
|
const os = require("os");
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
37
54
|
const path = require("path");
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
38
56
|
const fs = require("fs");
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
39
58
|
const { execFileSync, execSync } = require("child_process");
|
|
40
59
|
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
60
|
+
// Common absolute paths for `sqlite3`. Claude Desktop's MCP launcher
|
|
61
|
+
// strips PATH so the bare command name `sqlite3` would fail spawn
|
|
62
|
+
// even though `/usr/bin/sqlite3` is always present on macOS. Try the
|
|
63
|
+
// bare name first (cheap; works on dev machines with a normal shell)
|
|
64
|
+
// then walk known absolute paths.
|
|
65
|
+
const sqliteCandidates: string[] =
|
|
66
|
+
process.platform === "win32"
|
|
67
|
+
? ["sqlite3.exe", "C\\:Windows\\System32\\sqlite3.exe"]
|
|
68
|
+
: process.platform === "darwin"
|
|
69
|
+
? ["sqlite3", "/usr/bin/sqlite3", "/opt/homebrew/bin/sqlite3", "/usr/local/bin/sqlite3"]
|
|
70
|
+
: ["sqlite3", "/usr/bin/sqlite3", "/usr/local/bin/sqlite3"];
|
|
71
|
+
|
|
72
|
+
// 2. Direct sqlite3 read of the secret store. Only succeeds for
|
|
73
|
+
// plaintext entries (nonce all zeros). Encrypted entries fall
|
|
74
|
+
// through to the CLI path which can decrypt via keychain.
|
|
45
75
|
try {
|
|
46
76
|
const dbPath = path.join(os.homedir(), ".screenpipe", "db.sqlite");
|
|
47
77
|
if (fs.existsSync(dbPath)) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
let row: string | null = null;
|
|
79
|
+
for (const candidate of sqliteCandidates) {
|
|
80
|
+
try {
|
|
81
|
+
row = execFileSync(
|
|
82
|
+
candidate,
|
|
83
|
+
[dbPath, "SELECT hex(nonce), value FROM secrets WHERE key = 'api_auth_key';"],
|
|
84
|
+
{ timeout: 5000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] },
|
|
85
|
+
).trim();
|
|
86
|
+
break;
|
|
87
|
+
} catch {
|
|
88
|
+
// try next candidate
|
|
89
|
+
}
|
|
90
|
+
}
|
|
58
91
|
if (row) {
|
|
59
92
|
const sepIdx = row.indexOf("|");
|
|
60
93
|
const nonceHex = sepIdx >= 0 ? row.substring(0, sepIdx) : "";
|
|
@@ -65,32 +98,96 @@ function discoverApiKey(): string {
|
|
|
65
98
|
if (decoded && decoded.startsWith("sp-")) return decoded;
|
|
66
99
|
if (value.startsWith("sp-")) return value;
|
|
67
100
|
}
|
|
68
|
-
// Non-zero nonce = encrypted — fall through to
|
|
101
|
+
// Non-zero nonce = encrypted — fall through to bun/npx which decrypt via keychain.
|
|
69
102
|
}
|
|
70
103
|
}
|
|
71
104
|
} catch {}
|
|
72
105
|
|
|
73
|
-
//
|
|
106
|
+
// 3. Bundled `bun` shipped with the desktop app. The Tauri externalBin
|
|
107
|
+
// config (apps/screenpipe-app-tauri/src-tauri/tauri.prod.conf.json)
|
|
108
|
+
// places it next to the main app executable; on each OS the install
|
|
109
|
+
// path is deterministic so we don't need PATH or current_exe — both
|
|
110
|
+
// of which Claude Desktop's MCP launcher rolls back.
|
|
111
|
+
const home = os.homedir();
|
|
112
|
+
const bunCandidates: string[] =
|
|
113
|
+
process.platform === "darwin"
|
|
114
|
+
? [
|
|
115
|
+
// Standard system-wide install
|
|
116
|
+
"/Applications/screenpipe.app/Contents/MacOS/bun",
|
|
117
|
+
// Per-user install
|
|
118
|
+
path.join(home, "Applications", "screenpipe.app", "Contents", "MacOS", "bun"),
|
|
119
|
+
]
|
|
120
|
+
: process.platform === "win32"
|
|
121
|
+
? [
|
|
122
|
+
// NSIS per-user (default on Windows)
|
|
123
|
+
path.join(home, "AppData", "Local", "screenpipe", "bun.exe"),
|
|
124
|
+
// Per-user under "screenpipe-app" (older builds)
|
|
125
|
+
path.join(home, "AppData", "Local", "screenpipe-app", "bun.exe"),
|
|
126
|
+
// System-wide install
|
|
127
|
+
"C:\\Program Files\\screenpipe\\bun.exe",
|
|
128
|
+
]
|
|
129
|
+
: [
|
|
130
|
+
// Linux .deb
|
|
131
|
+
"/opt/screenpipe/bun",
|
|
132
|
+
"/usr/lib/screenpipe/bun",
|
|
133
|
+
"/usr/bin/bun",
|
|
134
|
+
];
|
|
135
|
+
for (const bunPath of bunCandidates) {
|
|
136
|
+
if (!fs.existsSync(bunPath)) continue;
|
|
137
|
+
try {
|
|
138
|
+
const token = execFileSync(bunPath, ["x", "screenpipe@latest", "auth", "token"], {
|
|
139
|
+
timeout: 30000, // first run downloads the package; subsequent runs are cached
|
|
140
|
+
encoding: "utf-8",
|
|
141
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
142
|
+
}).trim();
|
|
143
|
+
if (token && token.startsWith("sp-")) return token;
|
|
144
|
+
} catch {
|
|
145
|
+
// try next candidate
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 4. npx adjacent to the running node — works in dev environments
|
|
150
|
+
// where the user installed @screenpipe/mcp via npx without the
|
|
151
|
+
// desktop app.
|
|
74
152
|
try {
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
153
|
+
const npxName = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
154
|
+
const npxPath = path.join(path.dirname(process.execPath), npxName);
|
|
155
|
+
if (fs.existsSync(npxPath)) {
|
|
156
|
+
const token = execFileSync(npxPath, ["screenpipe@latest", "auth", "token"], {
|
|
157
|
+
timeout: 30000,
|
|
158
|
+
encoding: "utf-8",
|
|
159
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
160
|
+
}).trim();
|
|
161
|
+
if (token && token.startsWith("sp-")) return token;
|
|
162
|
+
}
|
|
82
163
|
} catch {}
|
|
83
164
|
|
|
84
|
-
//
|
|
165
|
+
// 5. PATH-based npx — last-ditch. Will fail under Claude Desktop's
|
|
166
|
+
// sanitized env; useful only on raw shells.
|
|
85
167
|
try {
|
|
86
168
|
const token = execSync("npx screenpipe@latest auth token", {
|
|
87
|
-
timeout:
|
|
169
|
+
timeout: 30000,
|
|
88
170
|
encoding: "utf-8",
|
|
89
171
|
stdio: ["pipe", "pipe", "pipe"],
|
|
90
172
|
}).trim();
|
|
91
|
-
if (token) return token;
|
|
173
|
+
if (token && token.startsWith("sp-")) return token;
|
|
92
174
|
} catch {}
|
|
93
175
|
|
|
176
|
+
// All five paths missed. Log loudly to stderr so the host's MCP
|
|
177
|
+
// panel surfaces this instead of the user seeing cryptic 403s from
|
|
178
|
+
// the screenpipe server on every tool call.
|
|
179
|
+
process.stderr.write(
|
|
180
|
+
[
|
|
181
|
+
"[screenpipe-mcp] could not discover SCREENPIPE_LOCAL_API_KEY from any source.",
|
|
182
|
+
" - env vars (SCREENPIPE_LOCAL_API_KEY / SCREENPIPE_API_KEY) not set",
|
|
183
|
+
" - direct sqlite3 read of ~/.screenpipe/db.sqlite failed",
|
|
184
|
+
" - bundled `bun` from screenpipe.app not found at any known install path",
|
|
185
|
+
" - npx fallback unavailable",
|
|
186
|
+
"Fix: set SCREENPIPE_LOCAL_API_KEY in your MCP launcher's env block,",
|
|
187
|
+
"or install the screenpipe desktop app (https://screenpi.pe).",
|
|
188
|
+
"",
|
|
189
|
+
].join("\n"),
|
|
190
|
+
);
|
|
94
191
|
return "";
|
|
95
192
|
}
|
|
96
193
|
|
|
@@ -426,6 +523,32 @@ const TOOLS: Tool[] = [
|
|
|
426
523
|
required: ["id"],
|
|
427
524
|
},
|
|
428
525
|
},
|
|
526
|
+
{
|
|
527
|
+
name: "update-meeting",
|
|
528
|
+
description:
|
|
529
|
+
"Update a meeting's mutable fields (title, attendees, note, app, start/end). Partial: only the fields you pass are written, " +
|
|
530
|
+
"others stay as-is. Use this to save an AI-generated summary into the meeting note — read the current note first via get-meeting " +
|
|
531
|
+
"and pass the existing notes plus your additions so you don't overwrite the user's writing. " +
|
|
532
|
+
"Convention: append AI-generated summary text under a `## Summary` heading at the bottom of the existing note.",
|
|
533
|
+
annotations: { title: "Update Meeting", readOnlyHint: false, destructiveHint: false, openWorldHint: false, idempotentHint: true },
|
|
534
|
+
inputSchema: {
|
|
535
|
+
type: "object",
|
|
536
|
+
properties: {
|
|
537
|
+
id: { type: "integer", description: "Meeting ID" },
|
|
538
|
+
title: { type: "string", description: "Meeting title" },
|
|
539
|
+
attendees: { type: "string", description: "Comma-separated attendee names" },
|
|
540
|
+
note: {
|
|
541
|
+
type: "string",
|
|
542
|
+
description:
|
|
543
|
+
"Full new note body. To preserve existing notes, fetch them first via get-meeting and concatenate before passing.",
|
|
544
|
+
},
|
|
545
|
+
meeting_app: { type: "string", description: "App / source name (e.g. 'meet.google.com', 'manual')" },
|
|
546
|
+
meeting_start: { type: "string", description: "ISO 8601 start time (rarely needed)" },
|
|
547
|
+
meeting_end: { type: "string", description: "ISO 8601 end time (rarely needed)" },
|
|
548
|
+
},
|
|
549
|
+
required: ["id"],
|
|
550
|
+
},
|
|
551
|
+
},
|
|
429
552
|
{
|
|
430
553
|
name: "keyword-search",
|
|
431
554
|
description:
|
|
@@ -1296,6 +1419,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1296
1419
|
};
|
|
1297
1420
|
}
|
|
1298
1421
|
|
|
1422
|
+
case "update-meeting": {
|
|
1423
|
+
const meetingId = args.id as number;
|
|
1424
|
+
if (!meetingId) {
|
|
1425
|
+
return { content: [{ type: "text", text: "Error: id is required" }] };
|
|
1426
|
+
}
|
|
1427
|
+
// Build partial body — only forward fields the caller provided.
|
|
1428
|
+
const body: Record<string, unknown> = {};
|
|
1429
|
+
for (const k of ["title", "attendees", "note", "meeting_app", "meeting_start", "meeting_end"] as const) {
|
|
1430
|
+
if (args[k] !== undefined && args[k] !== null) body[k] = args[k];
|
|
1431
|
+
}
|
|
1432
|
+
if (Object.keys(body).length === 0) {
|
|
1433
|
+
return {
|
|
1434
|
+
content: [
|
|
1435
|
+
{
|
|
1436
|
+
type: "text",
|
|
1437
|
+
text: "Error: pass at least one field to update (title, attendees, note, meeting_app, meeting_start, meeting_end).",
|
|
1438
|
+
},
|
|
1439
|
+
],
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
const response = await fetchAPI(`/meetings/${meetingId}`, {
|
|
1443
|
+
method: "PATCH",
|
|
1444
|
+
headers: { "Content-Type": "application/json" },
|
|
1445
|
+
body: JSON.stringify(body),
|
|
1446
|
+
});
|
|
1447
|
+
if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
|
|
1448
|
+
const updated = await response.json();
|
|
1449
|
+
return {
|
|
1450
|
+
content: [{ type: "text", text: JSON.stringify(updated, null, 2) }],
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1299
1454
|
case "keyword-search": {
|
|
1300
1455
|
const params = new URLSearchParams();
|
|
1301
1456
|
for (const [key, value] of Object.entries(args)) {
|