screenpipe-mcp 0.20.0 → 0.20.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.
@@ -0,0 +1,75 @@
1
+ // screenpipe — AI that knows everything you've seen, said, or heard
2
+ // https://screenpipe.com
3
+
4
+ import { readFileSync, writeFileSync, mkdirSync, mkdtempSync, rmSync, copyFileSync } from "node:fs";
5
+ import { execFileSync } from "node:child_process";
6
+ import { tmpdir } from "node:os";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { packExtension } from "@anthropic-ai/mcpb/cli";
10
+ import { zipSync } from "fflate";
11
+
12
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
13
+ const output = path.join(root, "installers");
14
+ const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
15
+ const read = (file) => readFileSync(path.join(root, file));
16
+ const json = (value) => Buffer.from(JSON.stringify(value, null, 2) + "\n");
17
+ const icon = readFileSync(path.join(root, "../../apps/screenpipe-app-tauri/src-tauri/icons/512x512.png"));
18
+ const license = readFileSync(path.join(root, "../../LICENSE.md"));
19
+
20
+ // Rebuild instead of shipping a stale dist/. Only explicitly listed files enter
21
+ // either archive, never credentials, local config, sources, or node_modules.
22
+ execFileSync("bun", ["run", "build"], { cwd: root, stdio: "inherit" });
23
+ mkdirSync(output, { recursive: true });
24
+ const stage = mkdtempSync(path.join(tmpdir(), "screenpipe-installers-"));
25
+ try {
26
+ const manifest = JSON.parse(read("manifest.json"));
27
+ manifest.version = pkg.version;
28
+ manifest.icon = "icon.png";
29
+ const localFiles = {
30
+ "manifest.json": json(manifest),
31
+ "package.json": json({ name: pkg.name, version: pkg.version, license: pkg.license }),
32
+ "dist/index.js": read("dist/index.js"),
33
+ "README.md": read("README.md"),
34
+ "LICENSE.md": license,
35
+ "icon.png": icon,
36
+ };
37
+ for (const [file, bytes] of Object.entries(localFiles)) {
38
+ const target = path.join(stage, file);
39
+ mkdirSync(path.dirname(target), { recursive: true });
40
+ writeFileSync(target, bytes);
41
+ }
42
+ const localArchive = path.join(output, "screenpipe-local.mcpb");
43
+ if (!await packExtension({ extensionPath: stage, outputPath: localArchive, silent: true })) {
44
+ throw new Error("MCPB validation or packaging failed");
45
+ }
46
+
47
+ const portable = JSON.parse(read("plugins/screenpipe-cloud/plugin.json"));
48
+ const mcp = JSON.parse(read("plugins/screenpipe-cloud/mcp.json"));
49
+ const { $schema, extensions, ...identity } = portable;
50
+ const legacyMcp = { mcpServers: Object.fromEntries(Object.entries(mcp.mcpServers)
51
+ .map(([name, config]) => [name, { ...config, type: "http" }])) };
52
+ const cloudFiles = {
53
+ "plugin.json": json(portable),
54
+ "mcp.json": json(mcp),
55
+ ".claude-plugin/plugin.json": json(identity),
56
+ ".codex-plugin/plugin.json": json({ ...identity, mcpServers: "./.mcp.json", ...extensions["com.openai"] }),
57
+ ".mcp.json": json(legacyMcp),
58
+ "assets/icon.png": icon,
59
+ "README.md": read("plugins/screenpipe-cloud/README.md"),
60
+ "LICENSE.md": license,
61
+ };
62
+ // Expose the same directory as the ZIP for local host validation/install.
63
+ const cloudDir = path.join(output, "screenpipe-cloud");
64
+ rmSync(cloudDir, { recursive: true, force: true });
65
+ for (const [file, bytes] of Object.entries(cloudFiles)) {
66
+ const target = path.join(cloudDir, file);
67
+ mkdirSync(path.dirname(target), { recursive: true });
68
+ writeFileSync(target, bytes);
69
+ }
70
+ writeFileSync(path.join(output, "screenpipe-cloud.zip"), zipSync(cloudFiles));
71
+ copyFileSync(path.join(root, "plugins/screenpipe-cloud/SUBMISSION.md"), path.join(output, "SUBMISSION.md"));
72
+ console.log(`Installers written to ${output}`);
73
+ } finally {
74
+ rmSync(stage, { recursive: true, force: true });
75
+ }
package/server.json CHANGED
@@ -8,13 +8,13 @@
8
8
  "source": "github",
9
9
  "subfolder": "packages/screenpipe-mcp"
10
10
  },
11
- "version": "0.20.0",
11
+ "version": "0.20.2",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "registryBaseUrl": "https://registry.npmjs.org",
16
16
  "identifier": "screenpipe-mcp",
17
- "version": "0.20.0",
17
+ "version": "0.20.2",
18
18
  "transport": {
19
19
  "type": "stdio"
20
20
  }
@@ -185,6 +185,67 @@ describe("isAuthorized", () => {
185
185
  });
186
186
 
187
187
  describe("buildHttpServer", () => {
188
+ it.each([
189
+ { name: "local API key", localKey: "upstream-secret", apiKey: "other-key", succeeds: true },
190
+ { name: "API key fallback", localKey: "", apiKey: "upstream-secret", succeeds: true },
191
+ { name: "missing upstream key", localKey: "", apiKey: "", succeeds: false },
192
+ { name: "invalid upstream key", localKey: "wrong-key", apiKey: "", succeeds: false },
193
+ ])("authenticates upstream independently of the MCP bearer: $name", async ({ localKey, apiKey, succeeds }) => {
194
+ const requests: Array<{ path: string; authorization: string | undefined }> = [];
195
+ const api = createServer((request, response) => {
196
+ requests.push({ path: request.url!, authorization: request.headers.authorization });
197
+ response.setHeader("content-type", "application/json");
198
+ if (request.headers.authorization !== "Bearer upstream-secret") {
199
+ response.writeHead(403).end(JSON.stringify({ error: "unauthorized" }));
200
+ return;
201
+ }
202
+ response.end(JSON.stringify({
203
+ data: [{ type: "OCR", content: { text: "synthetic authenticated result" } }],
204
+ pagination: { total: 1 },
205
+ }));
206
+ });
207
+ await new Promise<void>(resolve => api.listen(0, "127.0.0.1", resolve));
208
+ const apiPort = (api.address() as { port: number }).port;
209
+ vi.stubEnv("SCREENPIPE_API_URL", `http://127.0.0.1:${apiPort}`);
210
+ vi.stubEnv("SCREENPIPE_LOCAL_API_KEY", localKey);
211
+ vi.stubEnv("SCREENPIPE_API_KEY", apiKey);
212
+ const server = buildHttpServer({
213
+ mcpPort: 0,
214
+ screenpipePort: apiPort,
215
+ host: "127.0.0.1",
216
+ apiKey: "mcp-client-secret",
217
+ });
218
+ const client = new Client({ name: "integration-test", version: "1.0.0" });
219
+ try {
220
+ await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve));
221
+ const url = new URL(`http://127.0.0.1:${(server.address() as { port: number }).port}/mcp`);
222
+ await client.connect(new StreamableHTTPClientTransport(url, {
223
+ requestInit: { headers: { authorization: "Bearer mcp-client-secret" } },
224
+ }));
225
+ const call = client.callTool({ name: "search_content", arguments: { q: "fixture", limit: 1 } });
226
+ if (succeeds) {
227
+ const result = await call;
228
+ expect(result.isError).not.toBe(true);
229
+ expect(result.content).toEqual([expect.objectContaining({
230
+ type: "text", text: expect.stringContaining("synthetic authenticated result"),
231
+ })]);
232
+ await vi.waitFor(() => expect(requests.some(request => request.path === "/internal/telemetry/mcp-value")).toBe(true));
233
+ expect(requests.every(request => request.authorization === "Bearer upstream-secret")).toBe(true);
234
+ } else {
235
+ await expect(call).rejects.toThrow("HTTP error: 403");
236
+ expect(requests).toHaveLength(1);
237
+ }
238
+ expect(requests[0].path).toBe("/search?q=fixture&limit=1");
239
+ expect(requests.some(request => request.authorization === "Bearer mcp-client-secret")).toBe(false);
240
+ } finally {
241
+ await client.close();
242
+ server.closeAllConnections();
243
+ api.closeAllConnections();
244
+ await Promise.all([server, api].map(s => new Promise<void>(resolve => s.close(() => resolve()))));
245
+ vi.unstubAllEnvs();
246
+ }
247
+ });
248
+
188
249
  it("attributes retrievals per HTTP session and excludes empty or failed searches", async () => {
189
250
  const searches: Array<{ source: unknown; agent: unknown }> = [];
190
251
  const outcomes: unknown[] = [];
@@ -201,11 +201,13 @@ function makeFetchAPI(screenpipePort: number, client: () => McpClient) {
201
201
  /\/+$/,
202
202
  "",
203
203
  );
204
+ const apiKey = process.env.SCREENPIPE_LOCAL_API_KEY || process.env.SCREENPIPE_API_KEY;
204
205
  return async (endpoint: string, options: RequestInit = {}): Promise<Response> =>
205
206
  fetch(`${base}${endpoint}`, {
206
207
  ...options,
207
208
  headers: {
208
209
  "Content-Type": "application/json",
210
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
209
211
  "x-screenpipe-client": "mcp",
210
212
  "x-screenpipe-agent": client(),
211
213
  ...options.headers,
@@ -0,0 +1,143 @@
1
+ // screenpipe — AI that knows everything you've seen, said, or heard
2
+ // https://screenpipe.com
3
+
4
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
5
+ import { execFileSync } from "node:child_process";
6
+ import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import path from "node:path";
9
+ import { createServer, type Server } from "node:http";
10
+ import { unzipSync } from "fflate";
11
+ import { VERSIONED_MANIFEST_SCHEMAS } from "@anthropic-ai/mcpb/schemas";
12
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
13
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
14
+
15
+ const root = path.resolve(__dirname, "..");
16
+ const sandbox = mkdtempSync(path.join(tmpdir(), "screenpipe-installer-test-"));
17
+ let local: Record<string, Uint8Array>;
18
+ let cloud: Record<string, Uint8Array>;
19
+ let api: Server;
20
+ let baseUrl: string;
21
+ let status = 200;
22
+ let seenAuth: string | undefined;
23
+ let seenQuery: string | null;
24
+ const parse = (archive: Record<string, Uint8Array>, file: string) =>
25
+ JSON.parse(Buffer.from(archive[file]).toString("utf8"));
26
+
27
+ beforeAll(async () => {
28
+ execFileSync("bun", ["run", "build:installers"], { cwd: root, timeout: 120_000, stdio: "inherit" });
29
+ local = unzipSync(readFileSync(path.join(root, "installers/screenpipe-local.mcpb")));
30
+ cloud = unzipSync(readFileSync(path.join(root, "installers/screenpipe-cloud.zip")));
31
+ for (const [file, bytes] of Object.entries(local)) {
32
+ const target = path.join(sandbox, file);
33
+ mkdirSync(path.dirname(target), { recursive: true });
34
+ writeFileSync(target, bytes);
35
+ }
36
+ api = createServer((req, res) => {
37
+ const url = new URL(req.url!, "http://localhost");
38
+ if (url.pathname !== "/search") {
39
+ res.writeHead(200, { "Content-Type": "application/json" });
40
+ res.end("{}");
41
+ return;
42
+ }
43
+ seenAuth = req.headers.authorization;
44
+ seenQuery = url.searchParams.get("q");
45
+ res.writeHead(status, { "Content-Type": "application/json" });
46
+ res.end(JSON.stringify(status === 200 ? {
47
+ data: [{ type: "OCR", content: { text: "synthetic installer evidence", timestamp: "2026-01-02T12:00:00Z", app_name: "Fixture", frame_id: 1 } }],
48
+ pagination: { total: 1, limit: 5, offset: 0 },
49
+ } : { error: "unauthorized" }));
50
+ });
51
+ await new Promise<void>((resolve) => api.listen(0, "127.0.0.1", resolve));
52
+ baseUrl = `http://127.0.0.1:${(api.address() as { port: number }).port}`;
53
+ }, 130_000);
54
+
55
+ afterAll(async () => {
56
+ if (api) await new Promise<void>((resolve) => api.close(() => resolve()));
57
+ rmSync(sandbox, { recursive: true, force: true });
58
+ });
59
+
60
+ async function withInstalledClient(run: (client: Client) => Promise<void>) {
61
+ const manifest = parse(local, "manifest.json");
62
+ const config = manifest.server.mcp_config;
63
+ const settings: Record<string, string> = { api_url: baseUrl, api_key: "sp-installer-fixture" };
64
+ const env: Record<string, string> = {
65
+ PATH: "", NODE_PATH: "", SCREENPIPE_DISABLE_TELEMETRY: "1",
66
+ SCREENPIPE_ENTERPRISE_TOKEN: "sk_ent_fixture", SCREENPIPE_TEAM_API_URL: baseUrl,
67
+ };
68
+ for (const [key, value] of Object.entries(config.env)) {
69
+ env[key] = String(value).replace(/\$\{user_config\.(\w+)\}/g, (_, name) => settings[name]);
70
+ }
71
+ const transport = new StdioClientTransport({
72
+ command: process.execPath,
73
+ args: config.args.map((arg: string) => arg.replace("${__dirname}", sandbox)),
74
+ cwd: sandbox, env, stderr: "pipe",
75
+ });
76
+ const client = new Client({ name: "installer-fixture", version: "1.0.0" });
77
+ try {
78
+ await client.connect(transport);
79
+ await run(client);
80
+ } finally {
81
+ await transport.close();
82
+ }
83
+ }
84
+
85
+ describe("installable MCP artifacts", () => {
86
+ it("ships a schema-valid local extension with only the required files", () => {
87
+ const manifest = parse(local, "manifest.json");
88
+ expect(VERSIONED_MANIFEST_SCHEMAS["0.3"].safeParse(manifest).success).toBe(true);
89
+ expect(Object.keys(local).sort()).toEqual([
90
+ "LICENSE.md", "README.md", "dist/index.js", "icon.png", "manifest.json", "package.json",
91
+ ]);
92
+ expect(manifest.version).toBe(JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).version);
93
+ expect(manifest.user_config.api_key.sensitive).toBe(true);
94
+ expect(manifest.user_config.api_key.default).toBeUndefined();
95
+ expect(manifest.user_config.api_key.required).toBe(false);
96
+ expect(manifest.user_config.api_url.default).toBe("http://127.0.0.1:3030");
97
+ expect(local[manifest.server.entry_point]).toBeDefined();
98
+ });
99
+
100
+ it("initializes and searches from the extracted archive with no runtime dependencies", async () => {
101
+ status = 200;
102
+ await withInstalledClient(async (client) => {
103
+ const list = await client.listTools();
104
+ expect(list.tools.some((tool) => tool.name === "search-content")).toBe(true);
105
+ const result = await client.callTool({ name: "search-content", arguments: {
106
+ q: "synthetic", start_time: "2026-01-02T00:00:00Z", end_time: "2026-01-03T00:00:00Z", limit: 5,
107
+ } });
108
+ expect(result.isError).not.toBe(true);
109
+ expect(JSON.stringify(result)).toContain("synthetic installer evidence");
110
+ expect(seenAuth).toBe("Bearer sp-installer-fixture");
111
+ expect(seenQuery).toBe("synthetic");
112
+ });
113
+ }, 15_000);
114
+
115
+ it("reports rejected authentication rather than successful retrieval", async () => {
116
+ status = 401;
117
+ await withInstalledClient(async (client) => {
118
+ const result = await client.callTool({ name: "search-content", arguments: { q: "synthetic", limit: 1 } });
119
+ expect(result.isError).toBe(true);
120
+ expect(JSON.stringify(result)).not.toContain("synthetic installer evidence");
121
+ });
122
+ }, 15_000);
123
+
124
+ it("keeps all cloud hosts on the same OAuth endpoint without embedding credentials", () => {
125
+ expect(Object.keys(cloud).sort()).toEqual([
126
+ ".claude-plugin/plugin.json", ".codex-plugin/plugin.json", ".mcp.json",
127
+ "LICENSE.md", "README.md", "assets/icon.png", "mcp.json", "plugin.json",
128
+ ]);
129
+ const portable = parse(cloud, "plugin.json");
130
+ const codex = parse(cloud, ".codex-plugin/plugin.json");
131
+ const claude = parse(cloud, ".claude-plugin/plugin.json");
132
+ for (const manifest of [portable, codex, claude]) {
133
+ expect(manifest.name).toBe("screenpipe-cloud");
134
+ expect(manifest.version).toBe(portable.version);
135
+ }
136
+ const endpoint = "https://screenpipe.com/api/user/data-sync/mcp";
137
+ expect(parse(cloud, "mcp.json").mcpServers).toEqual({ "screenpipe-cloud": { type: "streamable-http", url: endpoint } });
138
+ expect(parse(cloud, ".mcp.json").mcpServers).toEqual({ "screenpipe-cloud": { type: "http", url: endpoint } });
139
+ expect(codex.mcpServers).toBe("./.mcp.json");
140
+ expect(codex.interface.capabilities).toEqual(["Read"]);
141
+ expect(cloud[codex.interface.logo.replace(/^\.\//, "")]).toBeDefined();
142
+ });
143
+ });
@@ -55,6 +55,7 @@ describe("createMcpQualifiedValueReporter", () => {
55
55
  ["chatgpt", "ChatGPT", "chatgpt"],
56
56
  ["gemini", "gemini-cli", "gemini"],
57
57
  ["runner", "Runner", "runner"],
58
+ ["vscode", "vscode", "vscode"],
58
59
  ["grokbot", "Grok Bot", "grokbot"],
59
60
  ])("recognizes configured %s and its handshake name", (configured, protocol, expected) => {
60
61
  expect(resolveMcpClient(configured, "unrecognized-client")).toBe(expected);
@@ -4,7 +4,7 @@
4
4
 
5
5
  const CLIENTS = [
6
6
  "claude", "chatgpt", "codex", "cursor", "gemini", "openclaw",
7
- "hermes", "runner", "windsurf", "grok", "grokbot",
7
+ "hermes", "runner", "windsurf", "vscode", "grok", "grokbot",
8
8
  ] as const;
9
9
  export type McpClient = (typeof CLIENTS)[number] | "unknown";
10
10