screenpipe-mcp 0.18.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.
Files changed (3) hide show
  1. package/dist/index.js +119 -28
  2. package/package.json +1 -1
  3. package/src/index.ts +124 -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: 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. 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
- // 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.
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
- 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,
79
- encoding: "utf-8",
80
- stdio: ["pipe", "pipe", "pipe"],
81
- }).trim();
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 CLI which can decrypt via keychain
122
+ // Non-zero nonce = encrypted — fall through to bun/npx which decrypt via keychain.
95
123
  }
96
124
  }
97
125
  }
98
126
  catch { }
99
- // Fallback: use the current Node binary to find npx (no PATH dependency)
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 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;
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
- // Last resort: npx on PATH
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: 15000,
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screenpipe-mcp",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
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,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: 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. 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
- // 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.
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
- 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,
55
- encoding: "utf-8",
56
- stdio: ["pipe", "pipe", "pipe"],
57
- }).trim();
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 CLI which can decrypt via keychain
101
+ // Non-zero nonce = encrypted — fall through to bun/npx which decrypt via keychain.
69
102
  }
70
103
  }
71
104
  } catch {}
72
105
 
73
- // Fallback: use the current Node binary to find npx (no PATH dependency)
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 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;
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
- // Last resort: npx on PATH
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: 15000,
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