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.
- package/dist/index.js +130 -38
- package/package.json +1 -1
- 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
|
|
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
|
-
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
//
|
|
195
|
+
// Encrypted — only the CLI paths above can decrypt this; we
|
|
196
|
+
// already tried them.
|
|
95
197
|
}
|
|
96
198
|
}
|
|
97
199
|
}
|
|
98
200
|
catch { }
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
//
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
//
|
|
171
|
+
// Encrypted — only the CLI paths above can decrypt this; we
|
|
172
|
+
// already tried them.
|
|
69
173
|
}
|
|
70
174
|
}
|
|
71
175
|
} catch {}
|
|
72
176
|
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|