screenpipe-mcp 0.18.0 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +130 -38
  2. package/package.json +1 -1
  3. package/src/index.ts +134 -36
package/dist/index.js CHANGED
@@ -53,32 +53,133 @@ for (let i = 0; i < args.length; i++) {
53
53
  }
54
54
  }
55
55
  const SCREENPIPE_API = `http://localhost:${port}`;
56
- // Discover API key: env var > db.sqlite direct read > npx fallbacks
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. CLI via bundled `bun` from screenpipe.app at a deterministic absolute
60
+ // path. Runs `bun x screenpipe@latest auth token` → goes through the
61
+ // Rust CLI's `find_api_auth_key` resolver, which handles the encrypted
62
+ // keychain-backed secret store. This is the canonical path: same
63
+ // contract as `screenpipe auth token` in a terminal, no PATH needed.
64
+ // 3. CLI via node-adjacent npx — for dev environments that have node but
65
+ // not the desktop app.
66
+ // 4. CLI via PATH-based npx — last CLI fallback.
67
+ // 5. Direct sqlite3 read of ~/.screenpipe/db.sqlite — plaintext entries
68
+ // only (encrypted entries need the keychain, which only the CLI can
69
+ // reach). Kept as a final last-resort for users who have screenpipe
70
+ // *data* but no working CLI install (rare). Demoted below the CLI
71
+ // paths because it reimplements logic that lives in `auth_key.rs` and
72
+ // can silently drift on storage-format changes.
73
+ //
74
+ // If all 5 miss we log a loud stderr warning so it surfaces in the host's
75
+ // MCP log instead of the user just seeing 403s with no explanation.
57
76
  function discoverApiKey() {
58
77
  const envKey = process.env.SCREENPIPE_LOCAL_API_KEY || process.env.SCREENPIPE_API_KEY;
59
78
  if (envKey)
60
79
  return envKey;
80
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
61
81
  const os = require("os");
82
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
62
83
  const path = require("path");
84
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
63
85
  const fs = require("fs");
86
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
64
87
  const { execFileSync, execSync } = require("child_process");
65
- // Read api_auth_key directly from ~/.screenpipe/db.sqlite.
66
- // The key may be stored as plaintext base64 (nonce=zeros, keychain unavailable)
67
- // or encrypted (non-zero nonce, keychain was available at write time).
68
- // If plaintext: decode and return. If encrypted: skip, fall through to CLI.
88
+ const home = os.homedir();
89
+ // 2. CLI via bundled `bun` shipped with the desktop app. The Tauri
90
+ // externalBin config places `bun` next to the main app exe at a
91
+ // deterministic install path on each OS, so we don't need PATH
92
+ // which Claude Desktop's MCP launcher strips. The CLI's `auth
93
+ // token` goes through `find_api_auth_key` and decrypts via
94
+ // keychain when needed.
95
+ const bunCandidates = process.platform === "darwin"
96
+ ? [
97
+ // Standard system-wide install
98
+ "/Applications/screenpipe.app/Contents/MacOS/bun",
99
+ // Per-user install
100
+ path.join(home, "Applications", "screenpipe.app", "Contents", "MacOS", "bun"),
101
+ ]
102
+ : process.platform === "win32"
103
+ ? [
104
+ // NSIS per-user (default on Windows)
105
+ path.join(home, "AppData", "Local", "screenpipe", "bun.exe"),
106
+ // Per-user under "screenpipe-app" (older builds)
107
+ path.join(home, "AppData", "Local", "screenpipe-app", "bun.exe"),
108
+ // System-wide install
109
+ "C:\\Program Files\\screenpipe\\bun.exe",
110
+ ]
111
+ : [
112
+ // Linux .deb
113
+ "/opt/screenpipe/bun",
114
+ "/usr/lib/screenpipe/bun",
115
+ "/usr/bin/bun",
116
+ ];
117
+ for (const bunPath of bunCandidates) {
118
+ if (!fs.existsSync(bunPath))
119
+ continue;
120
+ try {
121
+ const token = execFileSync(bunPath, ["x", "screenpipe@latest", "auth", "token"], {
122
+ timeout: 30000, // first run downloads the package; subsequent runs are cached
123
+ encoding: "utf-8",
124
+ stdio: ["pipe", "pipe", "pipe"],
125
+ }).trim();
126
+ if (token && token.startsWith("sp-"))
127
+ return token;
128
+ }
129
+ catch {
130
+ // try next candidate
131
+ }
132
+ }
133
+ // 3. CLI via npx adjacent to the running node. Works for dev
134
+ // environments without the desktop app.
69
135
  try {
70
- const dbPath = path.join(os.homedir(), ".screenpipe", "db.sqlite");
71
- if (fs.existsSync(dbPath)) {
72
- const sqliteBin = process.platform === "win32" ? "sqlite3.exe" : "sqlite3";
73
- // Check nonce all zeros means plaintext base64, non-zero means encrypted
74
- const row = execFileSync(sqliteBin, [
75
- dbPath,
76
- "SELECT hex(nonce), value FROM secrets WHERE key = 'api_auth_key';",
77
- ], {
78
- timeout: 5000,
136
+ const npxName = process.platform === "win32" ? "npx.cmd" : "npx";
137
+ const npxPath = path.join(path.dirname(process.execPath), npxName);
138
+ if (fs.existsSync(npxPath)) {
139
+ const token = execFileSync(npxPath, ["screenpipe@latest", "auth", "token"], {
140
+ timeout: 30000,
79
141
  encoding: "utf-8",
80
142
  stdio: ["pipe", "pipe", "pipe"],
81
143
  }).trim();
144
+ if (token && token.startsWith("sp-"))
145
+ return token;
146
+ }
147
+ }
148
+ catch { }
149
+ // 4. CLI via PATH-based npx. Last CLI try; works on raw shells with
150
+ // npx on PATH.
151
+ try {
152
+ const token = execSync("npx screenpipe@latest auth token", {
153
+ timeout: 30000,
154
+ encoding: "utf-8",
155
+ stdio: ["pipe", "pipe", "pipe"],
156
+ }).trim();
157
+ if (token && token.startsWith("sp-"))
158
+ return token;
159
+ }
160
+ catch { }
161
+ // 5. Direct sqlite3 read of the secret store (last-resort). Plaintext
162
+ // entries only — encrypted ones live behind the keychain, which the
163
+ // CLI paths above already cover. Used when the user has screenpipe
164
+ // data on disk but no working CLI install.
165
+ const sqliteCandidates = process.platform === "win32"
166
+ ? ["sqlite3.exe", "C:\\Windows\\System32\\sqlite3.exe"]
167
+ : process.platform === "darwin"
168
+ ? ["sqlite3", "/usr/bin/sqlite3", "/opt/homebrew/bin/sqlite3", "/usr/local/bin/sqlite3"]
169
+ : ["sqlite3", "/usr/bin/sqlite3", "/usr/local/bin/sqlite3"];
170
+ try {
171
+ const dbPath = path.join(home, ".screenpipe", "db.sqlite");
172
+ if (fs.existsSync(dbPath)) {
173
+ let row = null;
174
+ for (const candidate of sqliteCandidates) {
175
+ try {
176
+ 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();
177
+ break;
178
+ }
179
+ catch {
180
+ // try next candidate
181
+ }
182
+ }
82
183
  if (row) {
83
184
  const sepIdx = row.indexOf("|");
84
185
  const nonceHex = sepIdx >= 0 ? row.substring(0, sepIdx) : "";
@@ -91,34 +192,25 @@ function discoverApiKey() {
91
192
  if (value.startsWith("sp-"))
92
193
  return value;
93
194
  }
94
- // Non-zero nonce = encrypted fall through to CLI which can decrypt via keychain
195
+ // Encryptedonly the CLI paths above can decrypt this; we
196
+ // already tried them.
95
197
  }
96
198
  }
97
199
  }
98
200
  catch { }
99
- // Fallback: use the current Node binary to find npx (no PATH dependency)
100
- try {
101
- const npxPath = path.join(path.dirname(process.execPath), "npx");
102
- const token = execFileSync(npxPath, ["screenpipe@latest", "auth", "token"], {
103
- timeout: 15000,
104
- encoding: "utf-8",
105
- stdio: ["pipe", "pipe", "pipe"],
106
- }).trim();
107
- if (token)
108
- return token;
109
- }
110
- catch { }
111
- // Last resort: npx on PATH
112
- try {
113
- const token = execSync("npx screenpipe@latest auth token", {
114
- timeout: 15000,
115
- encoding: "utf-8",
116
- stdio: ["pipe", "pipe", "pipe"],
117
- }).trim();
118
- if (token)
119
- return token;
120
- }
121
- catch { }
201
+ // All five paths missed. Log loudly to stderr so the host's MCP
202
+ // panel surfaces this instead of the user seeing cryptic 403s from
203
+ // the screenpipe server on every tool call.
204
+ process.stderr.write([
205
+ "[screenpipe-mcp] could not discover SCREENPIPE_LOCAL_API_KEY from any source.",
206
+ " - env vars (SCREENPIPE_LOCAL_API_KEY / SCREENPIPE_API_KEY) not set",
207
+ " - bundled `bun` from screenpipe.app not found at any known install path",
208
+ " - npx fallback unavailable",
209
+ " - direct sqlite3 read of ~/.screenpipe/db.sqlite failed",
210
+ "Fix: set SCREENPIPE_LOCAL_API_KEY in your MCP launcher's env block,",
211
+ "or install the screenpipe desktop app (https://screenpi.pe).",
212
+ "",
213
+ ].join("\n"));
122
214
  return "";
123
215
  }
124
216
  const API_KEY = discoverApiKey();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screenpipe-mcp",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
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,33 +28,136 @@ for (let i = 0; i < args.length; i++) {
28
28
 
29
29
  const SCREENPIPE_API = `http://localhost:${port}`;
30
30
 
31
- // Discover API key: env var > db.sqlite direct read > npx fallbacks
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. CLI via bundled `bun` from screenpipe.app at a deterministic absolute
35
+ // path. Runs `bun x screenpipe@latest auth token` → goes through the
36
+ // Rust CLI's `find_api_auth_key` resolver, which handles the encrypted
37
+ // keychain-backed secret store. This is the canonical path: same
38
+ // contract as `screenpipe auth token` in a terminal, no PATH needed.
39
+ // 3. CLI via node-adjacent npx — for dev environments that have node but
40
+ // not the desktop app.
41
+ // 4. CLI via PATH-based npx — last CLI fallback.
42
+ // 5. Direct sqlite3 read of ~/.screenpipe/db.sqlite — plaintext entries
43
+ // only (encrypted entries need the keychain, which only the CLI can
44
+ // reach). Kept as a final last-resort for users who have screenpipe
45
+ // *data* but no working CLI install (rare). Demoted below the CLI
46
+ // paths because it reimplements logic that lives in `auth_key.rs` and
47
+ // can silently drift on storage-format changes.
48
+ //
49
+ // If all 5 miss we log a loud stderr warning so it surfaces in the host's
50
+ // MCP log instead of the user just seeing 403s with no explanation.
32
51
  function discoverApiKey(): string {
33
52
  const envKey = process.env.SCREENPIPE_LOCAL_API_KEY || process.env.SCREENPIPE_API_KEY;
34
53
  if (envKey) return envKey;
35
54
 
55
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
36
56
  const os = require("os");
57
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
37
58
  const path = require("path");
59
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
38
60
  const fs = require("fs");
61
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
39
62
  const { execFileSync, execSync } = require("child_process");
40
63
 
41
- // Read api_auth_key directly from ~/.screenpipe/db.sqlite.
42
- // The key may be stored as plaintext base64 (nonce=zeros, keychain unavailable)
43
- // or encrypted (non-zero nonce, keychain was available at write time).
44
- // If plaintext: decode and return. If encrypted: skip, fall through to CLI.
64
+ const home = os.homedir();
65
+
66
+ // 2. CLI via bundled `bun` shipped with the desktop app. The Tauri
67
+ // externalBin config places `bun` next to the main app exe at a
68
+ // deterministic install path on each OS, so we don't need PATH —
69
+ // which Claude Desktop's MCP launcher strips. The CLI's `auth
70
+ // token` goes through `find_api_auth_key` and decrypts via
71
+ // keychain when needed.
72
+ const bunCandidates: string[] =
73
+ process.platform === "darwin"
74
+ ? [
75
+ // Standard system-wide install
76
+ "/Applications/screenpipe.app/Contents/MacOS/bun",
77
+ // Per-user install
78
+ path.join(home, "Applications", "screenpipe.app", "Contents", "MacOS", "bun"),
79
+ ]
80
+ : process.platform === "win32"
81
+ ? [
82
+ // NSIS per-user (default on Windows)
83
+ path.join(home, "AppData", "Local", "screenpipe", "bun.exe"),
84
+ // Per-user under "screenpipe-app" (older builds)
85
+ path.join(home, "AppData", "Local", "screenpipe-app", "bun.exe"),
86
+ // System-wide install
87
+ "C:\\Program Files\\screenpipe\\bun.exe",
88
+ ]
89
+ : [
90
+ // Linux .deb
91
+ "/opt/screenpipe/bun",
92
+ "/usr/lib/screenpipe/bun",
93
+ "/usr/bin/bun",
94
+ ];
95
+ for (const bunPath of bunCandidates) {
96
+ if (!fs.existsSync(bunPath)) continue;
97
+ try {
98
+ const token = execFileSync(bunPath, ["x", "screenpipe@latest", "auth", "token"], {
99
+ timeout: 30000, // first run downloads the package; subsequent runs are cached
100
+ encoding: "utf-8",
101
+ stdio: ["pipe", "pipe", "pipe"],
102
+ }).trim();
103
+ if (token && token.startsWith("sp-")) return token;
104
+ } catch {
105
+ // try next candidate
106
+ }
107
+ }
108
+
109
+ // 3. CLI via npx adjacent to the running node. Works for dev
110
+ // environments without the desktop app.
45
111
  try {
46
- const dbPath = path.join(os.homedir(), ".screenpipe", "db.sqlite");
47
- if (fs.existsSync(dbPath)) {
48
- const sqliteBin = process.platform === "win32" ? "sqlite3.exe" : "sqlite3";
49
- // Check nonce all zeros means plaintext base64, non-zero means encrypted
50
- const row = execFileSync(sqliteBin, [
51
- dbPath,
52
- "SELECT hex(nonce), value FROM secrets WHERE key = 'api_auth_key';",
53
- ], {
54
- timeout: 5000,
112
+ const npxName = process.platform === "win32" ? "npx.cmd" : "npx";
113
+ const npxPath = path.join(path.dirname(process.execPath), npxName);
114
+ if (fs.existsSync(npxPath)) {
115
+ const token = execFileSync(npxPath, ["screenpipe@latest", "auth", "token"], {
116
+ timeout: 30000,
55
117
  encoding: "utf-8",
56
118
  stdio: ["pipe", "pipe", "pipe"],
57
119
  }).trim();
120
+ if (token && token.startsWith("sp-")) return token;
121
+ }
122
+ } catch {}
123
+
124
+ // 4. CLI via PATH-based npx. Last CLI try; works on raw shells with
125
+ // npx on PATH.
126
+ try {
127
+ const token = execSync("npx screenpipe@latest auth token", {
128
+ timeout: 30000,
129
+ encoding: "utf-8",
130
+ stdio: ["pipe", "pipe", "pipe"],
131
+ }).trim();
132
+ if (token && token.startsWith("sp-")) return token;
133
+ } catch {}
134
+
135
+ // 5. Direct sqlite3 read of the secret store (last-resort). Plaintext
136
+ // entries only — encrypted ones live behind the keychain, which the
137
+ // CLI paths above already cover. Used when the user has screenpipe
138
+ // data on disk but no working CLI install.
139
+ const sqliteCandidates: string[] =
140
+ process.platform === "win32"
141
+ ? ["sqlite3.exe", "C:\\Windows\\System32\\sqlite3.exe"]
142
+ : process.platform === "darwin"
143
+ ? ["sqlite3", "/usr/bin/sqlite3", "/opt/homebrew/bin/sqlite3", "/usr/local/bin/sqlite3"]
144
+ : ["sqlite3", "/usr/bin/sqlite3", "/usr/local/bin/sqlite3"];
145
+ try {
146
+ const dbPath = path.join(home, ".screenpipe", "db.sqlite");
147
+ if (fs.existsSync(dbPath)) {
148
+ let row: string | null = null;
149
+ for (const candidate of sqliteCandidates) {
150
+ try {
151
+ row = execFileSync(
152
+ candidate,
153
+ [dbPath, "SELECT hex(nonce), value FROM secrets WHERE key = 'api_auth_key';"],
154
+ { timeout: 5000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] },
155
+ ).trim();
156
+ break;
157
+ } catch {
158
+ // try next candidate
159
+ }
160
+ }
58
161
  if (row) {
59
162
  const sepIdx = row.indexOf("|");
60
163
  const nonceHex = sepIdx >= 0 ? row.substring(0, sepIdx) : "";
@@ -65,32 +168,27 @@ function discoverApiKey(): string {
65
168
  if (decoded && decoded.startsWith("sp-")) return decoded;
66
169
  if (value.startsWith("sp-")) return value;
67
170
  }
68
- // Non-zero nonce = encrypted fall through to CLI which can decrypt via keychain
171
+ // Encryptedonly the CLI paths above can decrypt this; we
172
+ // already tried them.
69
173
  }
70
174
  }
71
175
  } catch {}
72
176
 
73
- // Fallback: use the current Node binary to find npx (no PATH dependency)
74
- try {
75
- const npxPath = path.join(path.dirname(process.execPath), "npx");
76
- const token = execFileSync(npxPath, ["screenpipe@latest", "auth", "token"], {
77
- timeout: 15000,
78
- encoding: "utf-8",
79
- stdio: ["pipe", "pipe", "pipe"],
80
- }).trim();
81
- if (token) return token;
82
- } catch {}
83
-
84
- // Last resort: npx on PATH
85
- try {
86
- const token = execSync("npx screenpipe@latest auth token", {
87
- timeout: 15000,
88
- encoding: "utf-8",
89
- stdio: ["pipe", "pipe", "pipe"],
90
- }).trim();
91
- if (token) return token;
92
- } catch {}
93
-
177
+ // All five paths missed. Log loudly to stderr so the host's MCP
178
+ // panel surfaces this instead of the user seeing cryptic 403s from
179
+ // the screenpipe server on every tool call.
180
+ process.stderr.write(
181
+ [
182
+ "[screenpipe-mcp] could not discover SCREENPIPE_LOCAL_API_KEY from any source.",
183
+ " - env vars (SCREENPIPE_LOCAL_API_KEY / SCREENPIPE_API_KEY) not set",
184
+ " - bundled `bun` from screenpipe.app not found at any known install path",
185
+ " - npx fallback unavailable",
186
+ " - direct sqlite3 read of ~/.screenpipe/db.sqlite failed",
187
+ "Fix: set SCREENPIPE_LOCAL_API_KEY in your MCP launcher's env block,",
188
+ "or install the screenpipe desktop app (https://screenpi.pe).",
189
+ "",
190
+ ].join("\n"),
191
+ );
94
192
  return "";
95
193
  }
96
194