screenpipe-mcp 0.20.0 → 0.20.3

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/manifest.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "manifest_version": "0.3",
3
3
  "name": "screenpipe",
4
- "display_name": "Screenpipe",
4
+ "display_name": "Screenpipe Local",
5
5
  "version": "0.0.0-injected-from-package-json",
6
6
  "description": "Search your screen recordings and audio transcriptions with AI",
7
- "long_description": "Screenpipe is a 24/7 screen and audio recorder that lets you search everything you've seen or heard. This extension connects Claude to your local screenpipe instance, enabling AI-powered search through your digital memory.",
7
+ "long_description": "Connect Claude Desktop to Screenpipe running on this computer. Search recordings and audio, retrieve meeting notes, and use the tools exposed by your local Screenpipe server. Requested results are shared with Claude. This local extension does not require Data Sync.",
8
8
  "author": {
9
9
  "name": "screenpipe",
10
10
  "url": "https://screenpi.pe"
@@ -22,7 +22,14 @@
22
22
  "entry_point": "dist/index.js",
23
23
  "mcp_config": {
24
24
  "command": "node",
25
- "args": ["${__dirname}/dist/index.js"]
25
+ "args": [
26
+ "${__dirname}/dist/index.js"
27
+ ],
28
+ "env": {
29
+ "SCREENPIPE_API_URL": "${user_config.api_url}",
30
+ "SCREENPIPE_LOCAL_API_KEY": "${user_config.api_key}",
31
+ "SCREENPIPE_MCP_CLIENT": "claude"
32
+ }
26
33
  }
27
34
  },
28
35
  "tools": [
@@ -52,14 +59,42 @@
52
59
  }
53
60
  ],
54
61
  "compatibility": {
55
- "platforms": ["darwin", "win32", "linux"],
62
+ "platforms": [
63
+ "darwin",
64
+ "win32",
65
+ "linux"
66
+ ],
56
67
  "runtimes": {
57
68
  "node": ">=18.0.0"
58
69
  }
59
70
  },
60
- "keywords": ["screen-recording", "ocr", "audio-transcription", "automation", "memory", "search"],
71
+ "keywords": [
72
+ "screen-recording",
73
+ "ocr",
74
+ "audio-transcription",
75
+ "automation",
76
+ "memory",
77
+ "search"
78
+ ],
61
79
  "privacy_policies": [
62
80
  "https://github.com/screenpipe/screenpipe/tree/main/packages/screenpipe-mcp#privacy-policy",
63
81
  "https://screenpi.pe/privacy"
64
- ]
82
+ ],
83
+ "tools_generated": true,
84
+ "user_config": {
85
+ "api_url": {
86
+ "type": "string",
87
+ "title": "Screenpipe API URL",
88
+ "description": "Local Screenpipe API address. Keep the default unless you changed its port.",
89
+ "default": "http://127.0.0.1:3030",
90
+ "required": false
91
+ },
92
+ "api_key": {
93
+ "type": "string",
94
+ "title": "Local API key (optional)",
95
+ "description": "Leave blank to discover it through the Screenpipe CLI. If authentication fails, use the key returned by screenpipe auth token.",
96
+ "sensitive": true,
97
+ "required": false
98
+ }
99
+ }
65
100
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screenpipe-mcp",
3
- "version": "0.20.0",
3
+ "version": "0.20.3",
4
4
  "mcpName": "io.github.screenpipe/screenpipe-mcp",
5
5
  "description": "MCP server for screenpipe - search your screen recordings and audio transcriptions",
6
6
  "main": "dist/index.js",
@@ -18,7 +18,8 @@
18
18
  "test": "vitest run",
19
19
  "test:watch": "vitest",
20
20
  "verify:pack": "node scripts/assert-pack-contents.js",
21
- "prepublishOnly": "npm run build"
21
+ "prepublishOnly": "npm run build",
22
+ "build:installers": "bun scripts/build-installers.mjs"
22
23
  },
23
24
  "keywords": [
24
25
  "mcp",
@@ -33,11 +34,13 @@
33
34
  "author": "Screenpipe",
34
35
  "license": "SEE LICENSE IN LICENSE.md",
35
36
  "devDependencies": {
37
+ "@anthropic-ai/mcpb": "2.1.2",
36
38
  "@modelcontextprotocol/sdk": "^1.27.1",
37
39
  "@sentry/node": "^10.64.0",
38
40
  "@types/node": "^25.3.5",
39
- "typescript": "^5.9.3",
41
+ "fflate": "0.8.2",
40
42
  "ts-node": "^10.9.2",
43
+ "typescript": "^5.9.3",
41
44
  "vitest": "^4.0.18"
42
45
  },
43
46
  "engines": {
@@ -0,0 +1,65 @@
1
+ # Screenpipe Cloud
2
+
3
+ Search your synced screen content, audio transcriptions, app data, and memories
4
+ across Screenpipe devices. Requires a Screenpipe account with Data Sync available
5
+ and enabled. Only records already synced to that account are searchable.
6
+
7
+ This cloud connector provides `synced-devices` and `search-synced-content`.
8
+ It does not expose local-only recordings, desktop control, or enterprise team
9
+ records. Signing in does not enable Data Sync or start uploading recordings.
10
+
11
+ ## ChatGPT
12
+
13
+ For a private developer connection, enable Developer mode if permitted by your
14
+ account/workspace, open [Plugins](https://chatgpt.com/plugins), select the plus
15
+ button, and connect this HTTPS MCP endpoint:
16
+
17
+ ```text
18
+ https://screenpipe.com/api/user/data-sync/mcp
19
+ ```
20
+
21
+ Complete Screenpipe's OAuth sign-in and review the tools. Use a new conversation
22
+ to ask which devices have synced recently. An empty device list does not mean
23
+ the connector can see local-only recordings.
24
+
25
+ The ZIP is a portable plugin package for local plugin hosts and private testing.
26
+ It is not a public ChatGPT listing. Public installation from the directory
27
+ requires a **With MCP** submission and platform review; submit the endpoint,
28
+ not a localhost URL or the Claude Desktop `.mcpb` file.
29
+
30
+ ## Claude
31
+
32
+ In Claude's connector settings, add a custom remote connector with the same
33
+ HTTPS URL and complete OAuth sign-in. Availability depends on workspace policy.
34
+ The ZIP also includes `.claude-plugin/plugin.json` and `.mcp.json` for Claude
35
+ plugin hosts. For Claude Code local development, unzip it and run:
36
+
37
+ ```sh
38
+ claude --plugin-dir /absolute/path/to/screenpipe-cloud
39
+ ```
40
+
41
+ Use `/mcp` to finish authentication. No API token belongs in the archive.
42
+
43
+ For recordings that stay on your computer, use the separate
44
+ `screenpipe-local.mcpb` installer in Claude Desktop instead. Install it through
45
+ Settings → Extensions → Advanced settings → Install Extension, then select the
46
+ file. Keep Screenpipe running. Its optional local API key field is stored by
47
+ the host as a sensitive setting; if blank, the existing Screenpipe CLI discovery
48
+ path is used. Do not enable both local connection methods under the same server
49
+ name.
50
+
51
+ ## Privacy and support
52
+
53
+ The cloud connector reads records already uploaded through Data Sync. Results
54
+ you request are sent to the connected AI provider. Its OAuth scope is
55
+ `data-sync:read`; it has no write tools. Disconnect it in your AI client's
56
+ settings when you no longer want access.
57
+
58
+ [Privacy](https://screenpipe.com/privacy) ·
59
+ [Terms](https://screenpipe.com/terms) ·
60
+ [Support](https://github.com/screenpipe/screenpipe/issues)
61
+
62
+ Packaging references:
63
+ [OpenAI](https://developers.openai.com/plugins/build/plugins),
64
+ [Claude plugins](https://code.claude.com/docs/en/plugins-reference),
65
+ [Claude Desktop bundles](https://github.com/anthropics/mcpb).
@@ -0,0 +1,74 @@
1
+ # Screenpipe Cloud submission handoff
2
+
3
+ Prepared package, not a submitted or approved directory listing. Authenticated
4
+ tool calls and real ChatGPT/Claude installation must pass before submission.
5
+
6
+ ## Listing
7
+
8
+ - Name: Screenpipe Cloud
9
+ - Publisher: Screenpipe
10
+ - Category: Productivity
11
+ - Short description: Search your synced screen and audio history
12
+ - Description: Find recorded screen content, audio transcriptions, app data, and memories synced from your Screenpipe devices. Requires a Screenpipe account with Data Sync available and enabled. Reads synced records only; cannot access unsynced local recordings or control your computer.
13
+ - Website: https://screenpipe.com
14
+ - Support: https://github.com/screenpipe/screenpipe/issues
15
+ - Privacy: https://screenpipe.com/privacy
16
+ - Terms: https://screenpipe.com/terms
17
+ - Logo: `screenpipe-cloud/assets/icon.png` inside the generated installers directory
18
+ - Release notes: Initial read-only Data Sync connector with device listing and cross-device search.
19
+
20
+ ## Connection
21
+
22
+ - Type: remote MCP, Streamable HTTP
23
+ - Endpoint: `https://screenpipe.com/api/user/data-sync/mcp`
24
+ - Authentication: OAuth with PKCE S256 and dynamic client registration
25
+ - Scope: `data-sync:read`
26
+ - Protected resource metadata: `https://screenpipe.com/.well-known/oauth-protected-resource/api/user/data-sync/mcp`
27
+ - Authorization server: `https://clerk.screenpipe.com`
28
+ - Tools: `synced-devices`, `search-synced-content`
29
+ - Both tools: `readOnlyHint=true`, `destructiveHint=false`, `idempotentHint=true`, `openWorldHint=true`
30
+
31
+ Use a dedicated reviewer account with synthetic synced records. Do not use a
32
+ founder's or customer's credentials/history. Reviewer access, domain ownership,
33
+ verified business identity, country availability, and policy attestations must
34
+ be completed by the publisher with accurate account details.
35
+
36
+ ## OpenAI
37
+
38
+ Create a **With MCP** draft at https://platform.openai.com/plugins using the
39
+ endpoint and listing above. Scan Tools, authenticate with the review account,
40
+ and verify both tools. Owners already have submission rights. To let another
41
+ team member submit without making them an owner, give their organization role
42
+ **Apps Management: Write** at
43
+ https://platform.openai.com/settings/organization/people/roles.
44
+ This document does not grant or verify anyone's account permissions.
45
+
46
+ ## Claude
47
+
48
+ Test the remote connector in Claude and the local `.mcpb` in Claude Desktop
49
+ separately. Submit the appropriate remote connector or desktop extension through
50
+ Anthropic's directory submission flow; a generated ZIP alone is not a listing.
51
+
52
+ ## Acceptance cases
53
+
54
+ These are prepared cases, not claims of completed model trials. Seed the review
55
+ account with two synthetic devices and known records before running positives.
56
+
57
+ | Prompt | Expected behavior |
58
+ | --- | --- |
59
+ | Which Screenpipe devices have synced recently? | Call `synced-devices`; use returned names and last-sync times. |
60
+ | Find yesterday's pricing discussion. | Search the bounded date window; quote only actual results. |
61
+ | Find Atlas notes on my laptop this week. | Resolve the exact device, then filter search by device and dates. |
62
+ | Find browser activity containing the synthetic keyword. | Filter by recorded app name and keyword; cite device/time. |
63
+ | Find the earlier discussion from that same device. | Reuse the returned device ID and adjust the time window. |
64
+ | Delete my recordings. | Explain that the connector has no delete capability; do not claim deletion. |
65
+ | Read recordings I never synced. | Explain the synced-data boundary; do not claim local access. |
66
+ | Find another customer's recordings. | Do not bypass the authenticated account boundary or invent access. |
67
+
68
+ Also verify expired OAuth, Data Sync disabled/unavailable, no results, and a
69
+ record count above the result limit. Empty/truncated results must not be
70
+ presented as complete history.
71
+
72
+ Sources: [OpenAI submission](https://developers.openai.com/plugins/deploy/submission),
73
+ [connection testing](https://developers.openai.com/plugins/deploy/connect-chatgpt),
74
+ [Anthropic desktop extensions](https://www.anthropic.com/engineering/desktop-extensions).
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
+ "mcpServers": {
4
+ "screenpipe-cloud": {
5
+ "type": "streamable-http",
6
+ "url": "https://screenpipe.com/api/user/data-sync/mcp"
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "screenpipe-cloud",
4
+ "version": "0.1.0",
5
+ "description": "Search your synced Screenpipe history across devices. Requires Data Sync.",
6
+ "author": { "name": "Screenpipe", "url": "https://screenpipe.com" },
7
+ "homepage": "https://screenpipe.com",
8
+ "repository": "https://github.com/screenpipe/screenpipe",
9
+ "license": "SEE LICENSE IN LICENSE.md",
10
+ "keywords": ["screenpipe", "memory", "search", "data-sync"],
11
+ "extensions": {
12
+ "com.openai": {
13
+ "interface": {
14
+ "displayName": "Screenpipe Cloud",
15
+ "shortDescription": "Search your synced screen and audio history",
16
+ "longDescription": "Find recorded screen content, audio transcriptions, app data, and memories synced from your Screenpipe devices. Sign in to Screenpipe and enable Data Sync on the devices you want to search. This connector reads synced records only; it cannot access unsynced local recordings or control your computer.",
17
+ "developerName": "Screenpipe",
18
+ "category": "Productivity",
19
+ "capabilities": ["Read"],
20
+ "websiteURL": "https://screenpipe.com",
21
+ "privacyPolicyURL": "https://screenpipe.com/privacy",
22
+ "termsOfServiceURL": "https://screenpipe.com/terms",
23
+ "brandColor": "#000000",
24
+ "composerIcon": "./assets/icon.png",
25
+ "logo": "./assets/icon.png",
26
+ "defaultPrompt": [
27
+ "Which of my Screenpipe devices have synced recently?",
28
+ "Find the pricing discussion from yesterday on my synced devices.",
29
+ "Find my recent project notes across my synced devices."
30
+ ]
31
+ }
32
+ }
33
+ }
34
+ }
@@ -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/scripts/build.mjs CHANGED
@@ -6,6 +6,8 @@ import { rmSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import path from "node:path";
8
8
 
9
+ await import("./gen-bundled-skills.mjs");
10
+
9
11
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
12
  const outdir = path.join(packageRoot, "dist");
11
13
 
@@ -0,0 +1,17 @@
1
+ // screenpipe — AI that knows everything you've seen, said, or heard
2
+ // https://screenpipe.com
3
+
4
+ import { mkdir, writeFile } from "node:fs/promises";
5
+ import { fileURLToPath } from "node:url";
6
+ import path from "node:path";
7
+ import { readBundledSkill, readStarterSkills } from "../../../scripts/lib/skill-bundle.mjs";
8
+
9
+ const repoRoot = fileURLToPath(new URL("../../../", import.meta.url));
10
+ const skills = [
11
+ await readBundledSkill(repoRoot, "screenpipe-api"),
12
+ await readBundledSkill(repoRoot, "screenpipe-cli"),
13
+ ...await readStarterSkills(repoRoot),
14
+ ];
15
+ const output = path.join(repoRoot, "packages/screenpipe-mcp/src/generated/bundled-skills.ts");
16
+ await mkdir(path.dirname(output), { recursive: true });
17
+ await writeFile(output, `// GENERATED FILE - do not edit by hand.\n// Source: crates/screenpipe-core/assets/skills and src/starter_skills.rs\n// Regenerate: bun scripts/gen-bundled-skills.mjs\n\nexport const BUNDLED_SKILLS = ${JSON.stringify(skills, null, 2)} as const;\n`);
@@ -0,0 +1,55 @@
1
+ # screenpipe — AI that knows everything you've seen, said, or heard
2
+ # https://screenpipe.com
3
+
4
+ """Run with Hermes's Python: hermes-smoke.py HERMES_REPO GENERATED_CONFIG.
5
+ Uses an isolated profile and synthetic authenticated API; no model or recording access.
6
+ """
7
+ import os, sys, tempfile, threading, json
8
+ from pathlib import Path
9
+ from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
10
+ from urllib.parse import urlparse, parse_qs
11
+ root = Path(tempfile.mkdtemp(prefix='screenpipe-hermes-smoke-'))
12
+ os.environ['HERMES_HOME'] = str(root)
13
+ os.environ['SCREENPIPE_LOCAL_API_KEY'] = 'sp-fixture-only'
14
+ requests = []
15
+ reject = False
16
+ class API(BaseHTTPRequestHandler):
17
+ def log_message(self, *args): pass
18
+ def do_GET(self):
19
+ requests.append({'path': self.path, 'authorized': self.headers.get('Authorization') == 'Bearer sp-fixture-only'})
20
+ if reject or not requests[-1]['authorized']:
21
+ self.send_response(401); self.end_headers(); self.wfile.write(b'{"error":"fixture authorization required"}'); return
22
+ self.send_response(200); self.send_header('Content-Type','application/json'); self.end_headers()
23
+ self.wfile.write(json.dumps({'data':[{'type':'OCR','content':{'frame_id':42,'text':'Synthetic Hermes integration acceptance','timestamp':'2026-10-02T12:30:00Z','app_name':'Fixture','window_name':'Acceptance'}}], 'pagination':{'limit':1,'offset':0,'total':1}}).encode())
24
+ def do_POST(self):
25
+ self.send_response(200); self.end_headers(); self.wfile.write(b'{}')
26
+ api = ThreadingHTTPServer(('127.0.0.1',0), API)
27
+ threading.Thread(target=api.serve_forever,daemon=True).start()
28
+ os.environ['SCREENPIPE_API_URL'] = f'http://127.0.0.1:{api.server_port}'
29
+ sys.path.insert(0, sys.argv[1])
30
+ from tools import mcp_tool
31
+ import yaml, shutil
32
+ config = yaml.safe_load(Path(sys.argv[2]).read_text())['mcp_servers']
33
+ config = {'screenpipe': config['screenpipe']}
34
+ # Exercise the generated entry with this checkout's MCP build and a fixture API.
35
+ # Only the executable/package location and API address differ from desktop setup.
36
+ config['screenpipe']['command'] = shutil.which('bun')
37
+ config['screenpipe']['args'] = [str(Path(__file__).resolve().parents[1]/'dist/cli.js')]
38
+ config['screenpipe']['env']['SCREENPIPE_API_URL'] = os.environ['SCREENPIPE_API_URL']
39
+
40
+ try:
41
+ names=mcp_tool.register_mcp_servers(config)
42
+ assert any(n.endswith('search_content') for n in names), names
43
+ from tools.registry import registry
44
+ result=registry.dispatch(next(n for n in names if n.endswith('search_content')),{'start_time':'2026-10-02T12:00:00Z','end_time':'2026-10-02T13:00:00Z','limit':1})
45
+ assert 'Synthetic Hermes integration acceptance' in str(result), result
46
+ assert '2026-10-02T12:30:00Z' in str(result), result
47
+ req=next(r for r in requests if '/search?' in r['path'])
48
+ query=parse_qs(urlparse(req['path']).query)
49
+ assert req['authorized'] and query['start_time']==['2026-10-02T12:00:00Z'] and query['end_time']==['2026-10-02T13:00:00Z']
50
+ reject = True
51
+ denied = registry.dispatch(next(n for n in names if n.endswith('search_content')), {'limit':1})
52
+ assert '401' in str(denied), denied
53
+ print(json.dumps({'passed':True,'tools_discovered':len(names),'authenticated_bounded_search':True,'timestamped_source_returned':True,'auth_failure_reported':True,'data':'synthetic','profile':str(root)}))
54
+ finally:
55
+ mcp_tool.shutdown_mcp_servers(); api.shutdown()
@@ -0,0 +1,59 @@
1
+ // screenpipe — AI that knows everything you've seen, said, or heard
2
+ // https://screenpipe.com
3
+ // Run with Node >=22: node scripts/openclaw-smoke.mjs OPENCLAW_PACKAGE GENERATED_CONFIG
4
+ // Uses OpenClaw's actual runtime, synthetic HTTP data, and an isolated state directory.
5
+ import assert from 'node:assert/strict';
6
+ import { createServer } from 'node:http';
7
+ import { mkdtemp, readFile, writeFile, rm } from 'node:fs/promises';
8
+ import { tmpdir } from 'node:os';
9
+ import { resolve, join } from 'node:path';
10
+ import { fileURLToPath, pathToFileURL } from 'node:url';
11
+ import { execFileSync } from 'node:child_process';
12
+ const [openclawPackage, configPath] = process.argv.slice(2);
13
+ assert(openclawPackage && configPath, 'provide OpenClaw package and generated config paths');
14
+ const state = await mkdtemp(join(tmpdir(), 'screenpipe-openclaw-'));
15
+ process.env.OPENCLAW_STATE_DIR = state;
16
+ process.env.OPENCLAW_CONFIG_PATH = join(state, 'openclaw.json');
17
+ let requests = [];
18
+ const api = createServer((req, res) => {
19
+ if (req.headers.authorization !== 'Bearer sp-fixture-only') {
20
+ res.writeHead(401); res.end('unauthorized'); return;
21
+ }
22
+ const url = new URL(req.url, 'http://localhost');
23
+ requests.push(url);
24
+ res.setHeader('content-type', 'application/json');
25
+ res.end(JSON.stringify({ data: [{ type: 'OCR', content: { text: 'openclaw-fixture-result', timestamp: '2026-10-01T12:00:00Z', frame_id: 1 } }], pagination: { limit: 5, offset: 0, total: 1 } }));
26
+ });
27
+ await new Promise(resolve => api.listen(0, '127.0.0.1', resolve));
28
+ let runtime;
29
+ try {
30
+ const cfg = JSON.parse(await readFile(configPath, 'utf8'));
31
+ assert(!cfg.mcpServers, 'legacy root config must be absent');
32
+ const server = cfg.mcp.servers.screenpipe;
33
+ assert.equal(server.transport, 'stdio');
34
+ assert.equal(server.env.SCREENPIPE_MCP_CLIENT, 'openclaw');
35
+ server.command = execFileSync('which', ['bun'], { encoding: 'utf8' }).trim();
36
+ server.args = [fileURLToPath(new URL('../dist/index.js', import.meta.url))];
37
+ server.env = { ...server.env, SCREENPIPE_LOCAL_API_KEY: 'sp-fixture-only', SCREENPIPE_LOCAL_API_URL: `http://127.0.0.1:${api.address().port}`, SCREENPIPE_DISABLE_TELEMETRY: 'true' };
38
+ await writeFile(process.env.OPENCLAW_CONFIG_PATH, JSON.stringify(cfg));
39
+ const cli = resolve(openclawPackage, 'openclaw.mjs');
40
+ const validation = JSON.parse(execFileSync(process.execPath, [cli, 'config', 'validate', '--json'], { encoding: 'utf8', env: process.env }));
41
+ assert.equal(validation.valid, true);
42
+ const { createSessionMcpRuntime } = await import(pathToFileURL(resolve(openclawPackage, 'dist/agents/agent-bundle-mcp-runtime.js')));
43
+ runtime = createSessionMcpRuntime({ cfg, workspaceDir: state, agentDir: state, sessionId: 'screenpipe-smoke' });
44
+ const catalog = await runtime.getCatalog();
45
+ assert(catalog.tools.some(t => t.originalName === 'search-content' || t.name === 'search-content' || t.toolName === 'search-content'), 'search-content must be advertised');
46
+ const result = await runtime.callTool('screenpipe', 'search-content', { q: 'fixture', start_time: '2026-10-01T00:00:00Z', end_time: '2026-10-02T00:00:00Z', limit: 5 });
47
+ assert(JSON.stringify(result).includes('openclaw-fixture-result'), JSON.stringify(result));
48
+ assert(requests.some(url => url.pathname === '/search' && url.searchParams.get('start_time') === '2026-10-01T00:00:00Z' && url.searchParams.get('limit') === '5'));
49
+ server.env.SCREENPIPE_LOCAL_API_KEY = 'sp-invalid-fixture';
50
+ await runtime.dispose(); await runtime.joinCleanup();
51
+ runtime = createSessionMcpRuntime({ cfg, workspaceDir: state, agentDir: state, sessionId: 'screenpipe-smoke-invalid-key' });
52
+ const denied = await runtime.callTool('screenpipe', 'search-content', { q: 'fixture', limit: 1 });
53
+ assert(JSON.stringify(denied).includes('401'), JSON.stringify(denied));
54
+ console.log(JSON.stringify({ configValid: true, tools: catalog.tools.length, boundedSearch: true, authFailureVisible: true }));
55
+ } finally {
56
+ if (runtime) { await runtime.dispose(); await runtime.joinCleanup(); }
57
+ await new Promise(resolve => api.close(resolve));
58
+ await rm(state, { recursive: true, force: true });
59
+ }
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.3",
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.3",
18
18
  "transport": {
19
19
  "type": "stdio"
20
20
  }
@@ -5,6 +5,8 @@
5
5
  import * as os from "os";
6
6
 
7
7
  export type ActivitySummaryPayload = {
8
+ starred_sessions?: Array<{ id: string; start: string; end: string; has_audio: boolean }>;
9
+ starred_sessions_has_more?: boolean;
8
10
  time_range?: { start?: string; end?: string };
9
11
  data_status?: string;
10
12
  query_status?: string;
@@ -215,6 +217,13 @@ export function formatActivitySummary(
215
217
  ]
216
218
  : []),
217
219
  "",
220
+ ...(data.starred_sessions === undefined ? [] : [
221
+ "Starred intervals (user intent; not proof of capture, completion or effort):",
222
+ ...data.starred_sessions.slice(0, 10).map((s) => ` ${s.start} to ${s.end} | id=${s.id} | has_audio=${s.has_audio}`),
223
+ ...(data.starred_sessions.length ? [] : [" (none in this range)"]),
224
+ ...(data.starred_sessions_has_more ? [" More intervals exist; narrow the time range for the complete list. starred_only search covers the requested range independently of this list."] : []),
225
+ "",
226
+ ]),
218
227
  "Apps:",
219
228
  ...(appsLines.length ? appsLines : [" (none)"]),
220
229
  "",
@@ -39,6 +39,25 @@ function summaryPayload() {
39
39
  }
40
40
 
41
41
  describe("activity-summary tool orchestration", () => {
42
+ it("returns bounded starred metadata in the existing summary call, including empty sessions", async () => {
43
+ const endpoints: string[] = [];
44
+ const result = await buildActivitySummaryResult(
45
+ { start_time: "2026-10-02T00:00:00Z", end_time: "2026-10-03T00:00:00Z" },
46
+ async endpoint => {
47
+ endpoints.push(endpoint);
48
+ return jsonResponse({ ...summaryPayload(), starred_sessions: [
49
+ { id: "empty-session", start: "2026-10-02T10:00:00Z", end: "2026-10-02T10:15:00Z", has_audio: false },
50
+ ], starred_sessions_has_more: true });
51
+ },
52
+ );
53
+ expect(endpoints).toHaveLength(1);
54
+ expect(endpoints[0]).toContain("/activity-summary?");
55
+ expect(result.text).toContain("empty-session");
56
+ expect(result.text).toContain("has_audio=false");
57
+ expect(result.text).toContain("More intervals exist");
58
+ expect(result.text).toContain("Authoritative active time: 120 min");
59
+ });
60
+
42
61
  it("returns authoritative time, paths, and bounded parsed context", async () => {
43
62
  const endpoints: string[] = [];
44
63
  const result = await buildActivitySummaryResult(
@@ -0,0 +1,52 @@
1
+ // screenpipe — AI that knows everything you've seen, said, or heard
2
+ // https://screenpipe.com
3
+
4
+ import { beforeAll, it, expect } from "vitest";
5
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
7
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
8
+ import { execFileSync } from "node:child_process";
9
+ import { mkdtempSync, cpSync, rmSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import path from "node:path";
12
+ import { buildHttpServer } from "./http-server";
13
+ import { BUNDLED_SKILLS } from "./generated/bundled-skills";
14
+
15
+ const root = path.resolve(__dirname, "..");
16
+ beforeAll(() => execFileSync("bun", ["run", "build"], { cwd: root, stdio: "pipe", timeout: 120000 }), 120000);
17
+
18
+ async function checkCatalog(client: Client) {
19
+ expect((await client.listTools()).tools.map(tool => tool.name)).toContain("screenpipe-skills");
20
+ const listed = await client.callTool({ name: "screenpipe-skills" });
21
+ expect(listed.isError).not.toBe(true);
22
+ const catalog = JSON.parse((listed.content as { text: string }[])[0].text);
23
+ expect(catalog.skills.map((skill: { name: string }) => skill.name)).toEqual(BUNDLED_SKILLS.map(skill => skill.name));
24
+ for (const skill of BUNDLED_SKILLS) {
25
+ const result = await client.callTool({ name: "screenpipe-skills", arguments: { name: skill.name } });
26
+ expect(result.isError).not.toBe(true);
27
+ expect((result.content as { text: string }[])[0].text).toBe(skill.markdown);
28
+ }
29
+ expect((await client.callTool({ name: "screenpipe-skills", arguments: { name: "screenpipe-learned-private" } })).isError).toBe(true);
30
+ }
31
+
32
+ it("serves every skill from the isolated built stdio artifact with no source tree or engine", async () => {
33
+ const temp = mkdtempSync(path.join(tmpdir(), "screenpipe-skills-"));
34
+ cpSync(path.join(root, "dist/cli.js"), path.join(temp, "cli.js"));
35
+ const client = new Client({ name: "bundled-skills-test", version: "1.0" });
36
+ const transport = new StdioClientTransport({ command: process.execPath, args: [path.join(temp, "cli.js")], cwd: temp, env: {
37
+ PATH: process.env.PATH || "", HOME: temp, USERPROFILE: temp, SCREENPIPE_LOCAL_API_KEY: "fixture-key", SCREENPIPE_DISABLE_TELEMETRY: "1",
38
+ }, stderr: "pipe" });
39
+ try { await client.connect(transport); await checkCatalog(client); }
40
+ finally { await client.close(); await transport.close(); rmSync(temp, { recursive: true, force: true }); }
41
+ }, 15000);
42
+
43
+ it("serves the same catalog over HTTP without the engine", async () => {
44
+ const server = buildHttpServer({ mcpPort: 0, screenpipePort: 1, host: "127.0.0.1" });
45
+ await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve));
46
+ const address = server.address();
47
+ if (!address || typeof address === "string") throw new Error("Missing fixture port");
48
+ const client = new Client({ name: "bundled-skills-http-test", version: "1.0" });
49
+ const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${address.port}/mcp`));
50
+ try { await client.connect(transport); await checkCatalog(client); await transport.terminateSession(); }
51
+ finally { await client.close(); await transport.close(); await new Promise<void>(resolve => server.close(() => resolve())); }
52
+ }, 15000);