screenpipe-mcp 0.18.1 → 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.
- package/dist/index.js +65 -64
- package/package.json +1 -1
- package/src/index.ts +68 -67
package/dist/index.js
CHANGED
|
@@ -56,16 +56,20 @@ const SCREENPIPE_API = `http://localhost:${port}`;
|
|
|
56
56
|
// Discover the local API key, in priority order:
|
|
57
57
|
//
|
|
58
58
|
// 1. env vars set by the launcher (Claude Desktop config, terminal, etc.)
|
|
59
|
-
// 2.
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
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.
|
|
69
73
|
//
|
|
70
74
|
// If all 5 miss we log a loud stderr warning so it surfaces in the host's
|
|
71
75
|
// MCP log instead of the user just seeing 403s with no explanation.
|
|
@@ -81,55 +85,13 @@ function discoverApiKey() {
|
|
|
81
85
|
const fs = require("fs");
|
|
82
86
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
83
87
|
const { execFileSync, execSync } = require("child_process");
|
|
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.
|
|
97
|
-
try {
|
|
98
|
-
const dbPath = path.join(os.homedir(), ".screenpipe", "db.sqlite");
|
|
99
|
-
if (fs.existsSync(dbPath)) {
|
|
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
|
-
}
|
|
110
|
-
if (row) {
|
|
111
|
-
const sepIdx = row.indexOf("|");
|
|
112
|
-
const nonceHex = sepIdx >= 0 ? row.substring(0, sepIdx) : "";
|
|
113
|
-
const value = sepIdx >= 0 ? row.substring(sepIdx + 1) : row;
|
|
114
|
-
const isPlaintext = !nonceHex || /^0+$/.test(nonceHex);
|
|
115
|
-
if (isPlaintext && value) {
|
|
116
|
-
const decoded = Buffer.from(value, "base64").toString("utf-8");
|
|
117
|
-
if (decoded && decoded.startsWith("sp-"))
|
|
118
|
-
return decoded;
|
|
119
|
-
if (value.startsWith("sp-"))
|
|
120
|
-
return value;
|
|
121
|
-
}
|
|
122
|
-
// Non-zero nonce = encrypted — fall through to bun/npx which decrypt via keychain.
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
catch { }
|
|
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
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.
|
|
133
95
|
const bunCandidates = process.platform === "darwin"
|
|
134
96
|
? [
|
|
135
97
|
// Standard system-wide install
|
|
@@ -168,9 +130,8 @@ function discoverApiKey() {
|
|
|
168
130
|
// try next candidate
|
|
169
131
|
}
|
|
170
132
|
}
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
// desktop app.
|
|
133
|
+
// 3. CLI via npx adjacent to the running node. Works for dev
|
|
134
|
+
// environments without the desktop app.
|
|
174
135
|
try {
|
|
175
136
|
const npxName = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
176
137
|
const npxPath = path.join(path.dirname(process.execPath), npxName);
|
|
@@ -185,8 +146,8 @@ function discoverApiKey() {
|
|
|
185
146
|
}
|
|
186
147
|
}
|
|
187
148
|
catch { }
|
|
188
|
-
//
|
|
189
|
-
//
|
|
149
|
+
// 4. CLI via PATH-based npx. Last CLI try; works on raw shells with
|
|
150
|
+
// npx on PATH.
|
|
190
151
|
try {
|
|
191
152
|
const token = execSync("npx screenpipe@latest auth token", {
|
|
192
153
|
timeout: 30000,
|
|
@@ -197,15 +158,55 @@ function discoverApiKey() {
|
|
|
197
158
|
return token;
|
|
198
159
|
}
|
|
199
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
|
+
}
|
|
183
|
+
if (row) {
|
|
184
|
+
const sepIdx = row.indexOf("|");
|
|
185
|
+
const nonceHex = sepIdx >= 0 ? row.substring(0, sepIdx) : "";
|
|
186
|
+
const value = sepIdx >= 0 ? row.substring(sepIdx + 1) : row;
|
|
187
|
+
const isPlaintext = !nonceHex || /^0+$/.test(nonceHex);
|
|
188
|
+
if (isPlaintext && value) {
|
|
189
|
+
const decoded = Buffer.from(value, "base64").toString("utf-8");
|
|
190
|
+
if (decoded && decoded.startsWith("sp-"))
|
|
191
|
+
return decoded;
|
|
192
|
+
if (value.startsWith("sp-"))
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
// Encrypted — only the CLI paths above can decrypt this; we
|
|
196
|
+
// already tried them.
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch { }
|
|
200
201
|
// All five paths missed. Log loudly to stderr so the host's MCP
|
|
201
202
|
// panel surfaces this instead of the user seeing cryptic 403s from
|
|
202
203
|
// the screenpipe server on every tool call.
|
|
203
204
|
process.stderr.write([
|
|
204
205
|
"[screenpipe-mcp] could not discover SCREENPIPE_LOCAL_API_KEY from any source.",
|
|
205
206
|
" - env vars (SCREENPIPE_LOCAL_API_KEY / SCREENPIPE_API_KEY) not set",
|
|
206
|
-
" - direct sqlite3 read of ~/.screenpipe/db.sqlite failed",
|
|
207
207
|
" - bundled `bun` from screenpipe.app not found at any known install path",
|
|
208
208
|
" - npx fallback unavailable",
|
|
209
|
+
" - direct sqlite3 read of ~/.screenpipe/db.sqlite failed",
|
|
209
210
|
"Fix: set SCREENPIPE_LOCAL_API_KEY in your MCP launcher's env block,",
|
|
210
211
|
"or install the screenpipe desktop app (https://screenpi.pe).",
|
|
211
212
|
"",
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -31,16 +31,20 @@ const SCREENPIPE_API = `http://localhost:${port}`;
|
|
|
31
31
|
// Discover the local API key, in priority order:
|
|
32
32
|
//
|
|
33
33
|
// 1. env vars set by the launcher (Claude Desktop config, terminal, etc.)
|
|
34
|
-
// 2.
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
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.
|
|
44
48
|
//
|
|
45
49
|
// If all 5 miss we log a loud stderr warning so it surfaces in the host's
|
|
46
50
|
// MCP log instead of the user just seeing 403s with no explanation.
|
|
@@ -57,58 +61,14 @@ function discoverApiKey(): string {
|
|
|
57
61
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
58
62
|
const { execFileSync, execSync } = require("child_process");
|
|
59
63
|
|
|
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.
|
|
75
|
-
try {
|
|
76
|
-
const dbPath = path.join(os.homedir(), ".screenpipe", "db.sqlite");
|
|
77
|
-
if (fs.existsSync(dbPath)) {
|
|
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
|
-
}
|
|
91
|
-
if (row) {
|
|
92
|
-
const sepIdx = row.indexOf("|");
|
|
93
|
-
const nonceHex = sepIdx >= 0 ? row.substring(0, sepIdx) : "";
|
|
94
|
-
const value = sepIdx >= 0 ? row.substring(sepIdx + 1) : row;
|
|
95
|
-
const isPlaintext = !nonceHex || /^0+$/.test(nonceHex);
|
|
96
|
-
if (isPlaintext && value) {
|
|
97
|
-
const decoded = Buffer.from(value, "base64").toString("utf-8");
|
|
98
|
-
if (decoded && decoded.startsWith("sp-")) return decoded;
|
|
99
|
-
if (value.startsWith("sp-")) return value;
|
|
100
|
-
}
|
|
101
|
-
// Non-zero nonce = encrypted — fall through to bun/npx which decrypt via keychain.
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
} catch {}
|
|
105
|
-
|
|
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
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.
|
|
112
72
|
const bunCandidates: string[] =
|
|
113
73
|
process.platform === "darwin"
|
|
114
74
|
? [
|
|
@@ -146,9 +106,8 @@ function discoverApiKey(): string {
|
|
|
146
106
|
}
|
|
147
107
|
}
|
|
148
108
|
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
// desktop app.
|
|
109
|
+
// 3. CLI via npx adjacent to the running node. Works for dev
|
|
110
|
+
// environments without the desktop app.
|
|
152
111
|
try {
|
|
153
112
|
const npxName = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
154
113
|
const npxPath = path.join(path.dirname(process.execPath), npxName);
|
|
@@ -162,8 +121,8 @@ function discoverApiKey(): string {
|
|
|
162
121
|
}
|
|
163
122
|
} catch {}
|
|
164
123
|
|
|
165
|
-
//
|
|
166
|
-
//
|
|
124
|
+
// 4. CLI via PATH-based npx. Last CLI try; works on raw shells with
|
|
125
|
+
// npx on PATH.
|
|
167
126
|
try {
|
|
168
127
|
const token = execSync("npx screenpipe@latest auth token", {
|
|
169
128
|
timeout: 30000,
|
|
@@ -173,6 +132,48 @@ function discoverApiKey(): string {
|
|
|
173
132
|
if (token && token.startsWith("sp-")) return token;
|
|
174
133
|
} catch {}
|
|
175
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
|
+
}
|
|
161
|
+
if (row) {
|
|
162
|
+
const sepIdx = row.indexOf("|");
|
|
163
|
+
const nonceHex = sepIdx >= 0 ? row.substring(0, sepIdx) : "";
|
|
164
|
+
const value = sepIdx >= 0 ? row.substring(sepIdx + 1) : row;
|
|
165
|
+
const isPlaintext = !nonceHex || /^0+$/.test(nonceHex);
|
|
166
|
+
if (isPlaintext && value) {
|
|
167
|
+
const decoded = Buffer.from(value, "base64").toString("utf-8");
|
|
168
|
+
if (decoded && decoded.startsWith("sp-")) return decoded;
|
|
169
|
+
if (value.startsWith("sp-")) return value;
|
|
170
|
+
}
|
|
171
|
+
// Encrypted — only the CLI paths above can decrypt this; we
|
|
172
|
+
// already tried them.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch {}
|
|
176
|
+
|
|
176
177
|
// All five paths missed. Log loudly to stderr so the host's MCP
|
|
177
178
|
// panel surfaces this instead of the user seeing cryptic 403s from
|
|
178
179
|
// the screenpipe server on every tool call.
|
|
@@ -180,9 +181,9 @@ function discoverApiKey(): string {
|
|
|
180
181
|
[
|
|
181
182
|
"[screenpipe-mcp] could not discover SCREENPIPE_LOCAL_API_KEY from any source.",
|
|
182
183
|
" - env vars (SCREENPIPE_LOCAL_API_KEY / SCREENPIPE_API_KEY) not set",
|
|
183
|
-
" - direct sqlite3 read of ~/.screenpipe/db.sqlite failed",
|
|
184
184
|
" - bundled `bun` from screenpipe.app not found at any known install path",
|
|
185
185
|
" - npx fallback unavailable",
|
|
186
|
+
" - direct sqlite3 read of ~/.screenpipe/db.sqlite failed",
|
|
186
187
|
"Fix: set SCREENPIPE_LOCAL_API_KEY in your MCP launcher's env block,",
|
|
187
188
|
"or install the screenpipe desktop app (https://screenpi.pe).",
|
|
188
189
|
"",
|