clickshot-mcp 0.1.0
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/README.md +61 -0
- package/package.json +34 -0
- package/server.js +207 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# clickshot-mcp
|
|
2
|
+
|
|
3
|
+
Local MCP server for the **ClickShot** Chrome extension. It lets the Claude you're
|
|
4
|
+
already running (Claude Code / Claude Desktop) see your recent browser activity —
|
|
5
|
+
on your own subscription, with **no API key and no billing**. Everything stays on
|
|
6
|
+
your machine.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
ClickShot extension ──POST screenshot──> clickshot-mcp ──MCP──> your Claude
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Run
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx clickshot-mcp # HTTP mode on 127.0.0.1:7333
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
clickshot-mcp [--stdio] [--port <n>]
|
|
22
|
+
(default) HTTP mode with an /mcp endpoint (for Claude Code)
|
|
23
|
+
--stdio stdio MCP (for Claude Desktop); still opens the ingest port
|
|
24
|
+
--port <n> ingest / MCP port (default 7333, or $CLICKSHOT_PORT)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Connect Claude Code (HTTP)
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx clickshot-mcp # leave running
|
|
31
|
+
claude mcp add --transport http clickshot http://127.0.0.1:7333/mcp
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Connect Claude Desktop (stdio)
|
|
35
|
+
|
|
36
|
+
Add to `claude_desktop_config.json`
|
|
37
|
+
(macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"clickshot": { "command": "npx", "args": ["-y", "clickshot-mcp", "--stdio"] }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Restart Claude Desktop. The server starts automatically and also opens the
|
|
48
|
+
extension ingest port.
|
|
49
|
+
|
|
50
|
+
## Tools
|
|
51
|
+
|
|
52
|
+
- `get_recent_activity(limit)` — last N clicks as annotated screenshots + a log
|
|
53
|
+
(URL, page title, clicked element, text)
|
|
54
|
+
- `clear_activity()` — wipe the buffer
|
|
55
|
+
|
|
56
|
+
## Ingest API (used by the extension)
|
|
57
|
+
|
|
58
|
+
- `POST /capture` — `{ image: base64, mimeType, meta }`
|
|
59
|
+
- `GET /health` — `{ ok, captures, mode }`
|
|
60
|
+
|
|
61
|
+
Buffers the most recent 200 captures in memory. Binds to `127.0.0.1` only.
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clickshot-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local MCP server that feeds ClickShot screenshots into your running Claude (Code/Desktop) on your own subscription.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"clickshot-mcp": "server.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"server.js",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node server.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"claude",
|
|
22
|
+
"screenshots",
|
|
23
|
+
"workflow",
|
|
24
|
+
"browser",
|
|
25
|
+
"clickshot"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
30
|
+
"cors": "^2.8.5",
|
|
31
|
+
"express": "^4.21.2",
|
|
32
|
+
"zod": "^3.24.1"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ClickShot MCP server.
|
|
3
|
+
//
|
|
4
|
+
// Bridges the ClickShot browser extension to the user's own Claude. Everything
|
|
5
|
+
// stays local — no API key, no billing. Two transport modes:
|
|
6
|
+
//
|
|
7
|
+
// HTTP (default) — long-running server. Register in Claude Code:
|
|
8
|
+
// claude mcp add --transport http clickshot http://127.0.0.1:7333/mcp
|
|
9
|
+
// stdio (--stdio) — launched by Claude Desktop via its config. Also opens the
|
|
10
|
+
// ingest port so the extension can still push screenshots.
|
|
11
|
+
//
|
|
12
|
+
// In both modes the extension POSTs screenshots to http://127.0.0.1:<port>/capture.
|
|
13
|
+
|
|
14
|
+
import express from "express";
|
|
15
|
+
import cors from "cors";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
19
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
|
+
|
|
21
|
+
const VERSION = "0.1.0";
|
|
22
|
+
|
|
23
|
+
// stdout is the JSON-RPC channel in stdio mode, so all logging goes to stderr.
|
|
24
|
+
const log = (...a) => console.error(...a);
|
|
25
|
+
|
|
26
|
+
// ---- args ----
|
|
27
|
+
const argv = process.argv.slice(2);
|
|
28
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
29
|
+
process.stdout.write(
|
|
30
|
+
`clickshot-mcp v${VERSION}\n\n` +
|
|
31
|
+
`Usage: clickshot-mcp [--stdio] [--port <n>]\n\n` +
|
|
32
|
+
` (default) HTTP mode on 127.0.0.1:<port> with an /mcp endpoint\n` +
|
|
33
|
+
` --stdio stdio MCP (for Claude Desktop); still opens the ingest port\n` +
|
|
34
|
+
` --port <n> port for the extension ingest / HTTP MCP (default 7333,\n` +
|
|
35
|
+
` or $CLICKSHOT_PORT)\n`
|
|
36
|
+
);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
40
|
+
process.stdout.write(VERSION + "\n");
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
const portFlagIdx = argv.indexOf("--port");
|
|
44
|
+
const PORT = Number(
|
|
45
|
+
(portFlagIdx !== -1 && argv[portFlagIdx + 1]) || process.env.CLICKSHOT_PORT || 7333
|
|
46
|
+
);
|
|
47
|
+
const STDIO = argv.includes("--stdio");
|
|
48
|
+
|
|
49
|
+
const MAX_BUFFER = 200; // keep the most recent N captures in memory
|
|
50
|
+
|
|
51
|
+
/** @type {Array<{id:number, image:string, mimeType:string, meta:object}>} */
|
|
52
|
+
const captures = [];
|
|
53
|
+
let nextId = 1;
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// MCP server definition (tools)
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
function buildMcpServer() {
|
|
59
|
+
const server = new McpServer({ name: "clickshot", version: VERSION });
|
|
60
|
+
|
|
61
|
+
server.registerTool(
|
|
62
|
+
"get_recent_activity",
|
|
63
|
+
{
|
|
64
|
+
title: "Get recent browser activity",
|
|
65
|
+
description:
|
|
66
|
+
"Returns the user's most recent browser clicks as annotated screenshots " +
|
|
67
|
+
"plus a log of what was clicked (URL, page title, element, text). Use this " +
|
|
68
|
+
"to understand what the user has been doing in their browser.",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
limit: z
|
|
71
|
+
.number()
|
|
72
|
+
.int()
|
|
73
|
+
.min(1)
|
|
74
|
+
.max(25)
|
|
75
|
+
.optional()
|
|
76
|
+
.describe("How many of the most recent clicks to return (default 10)."),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
async ({ limit }) => {
|
|
80
|
+
const n = Math.min(limit || 10, 25);
|
|
81
|
+
const recent = captures.slice(-n);
|
|
82
|
+
if (recent.length === 0) {
|
|
83
|
+
return {
|
|
84
|
+
content: [
|
|
85
|
+
{ type: "text", text: "No browser activity captured yet. The user may need to start recording in the ClickShot extension." },
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const content = [
|
|
91
|
+
{
|
|
92
|
+
type: "text",
|
|
93
|
+
text:
|
|
94
|
+
`Most recent ${recent.length} browser click(s), oldest first. ` +
|
|
95
|
+
`Each screenshot has a red marker at the click point.`,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
for (const c of recent) {
|
|
100
|
+
const m = c.meta || {};
|
|
101
|
+
const when = m.timestamp ? new Date(m.timestamp).toISOString() : "unknown time";
|
|
102
|
+
const what =
|
|
103
|
+
(m.tag ? `<${m.tag}>` : "element") +
|
|
104
|
+
(m.text ? ` "${m.text}"` : "") +
|
|
105
|
+
(m.selector ? ` [${m.selector}]` : "");
|
|
106
|
+
content.push({
|
|
107
|
+
type: "text",
|
|
108
|
+
text: `#${c.id} ${when} — ${m.title || ""}\n ${m.url || ""}\n clicked ${what}`,
|
|
109
|
+
});
|
|
110
|
+
content.push({ type: "image", data: c.image, mimeType: c.mimeType || "image/jpeg" });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { content };
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
server.registerTool(
|
|
118
|
+
"clear_activity",
|
|
119
|
+
{
|
|
120
|
+
title: "Clear captured activity",
|
|
121
|
+
description: "Discards all buffered browser screenshots from the ClickShot server.",
|
|
122
|
+
inputSchema: {},
|
|
123
|
+
},
|
|
124
|
+
async () => {
|
|
125
|
+
const count = captures.length;
|
|
126
|
+
captures.length = 0;
|
|
127
|
+
return { content: [{ type: "text", text: `Cleared ${count} captured screenshot(s).` }] };
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return server;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Ingest HTTP app (used in BOTH modes so the extension can push screenshots)
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
function buildIngestApp() {
|
|
138
|
+
const app = express();
|
|
139
|
+
app.use(cors()); // allow the extension (chrome-extension://) origin
|
|
140
|
+
app.use(express.json({ limit: "25mb" })); // screenshots are base64
|
|
141
|
+
|
|
142
|
+
app.post("/capture", (req, res) => {
|
|
143
|
+
const { image, mimeType, meta } = req.body || {};
|
|
144
|
+
if (!image) return res.status(400).json({ error: "missing image" });
|
|
145
|
+
captures.push({ id: nextId++, image, mimeType: mimeType || "image/jpeg", meta: meta || {} });
|
|
146
|
+
if (captures.length > MAX_BUFFER) captures.splice(0, captures.length - MAX_BUFFER);
|
|
147
|
+
res.json({ ok: true, buffered: captures.length });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
app.get("/health", (_req, res) => res.json({ ok: true, captures: captures.length, mode: STDIO ? "stdio" : "http" }));
|
|
151
|
+
return app;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Boot
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
async function main() {
|
|
158
|
+
if (STDIO) {
|
|
159
|
+
// Claude Desktop launches this; talk MCP over stdio, ingest over HTTP.
|
|
160
|
+
const app = buildIngestApp();
|
|
161
|
+
app.listen(PORT, "127.0.0.1", () => {
|
|
162
|
+
log(`ClickShot ingest on http://127.0.0.1:${PORT}/capture (stdio MCP mode)`);
|
|
163
|
+
});
|
|
164
|
+
const server = buildMcpServer();
|
|
165
|
+
const transport = new StdioServerTransport();
|
|
166
|
+
await server.connect(transport);
|
|
167
|
+
log("ClickShot MCP ready over stdio.");
|
|
168
|
+
} else {
|
|
169
|
+
// Long-running HTTP server with both ingest and an /mcp endpoint.
|
|
170
|
+
const app = buildIngestApp();
|
|
171
|
+
|
|
172
|
+
app.post("/mcp", async (req, res) => {
|
|
173
|
+
try {
|
|
174
|
+
const server = buildMcpServer();
|
|
175
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
176
|
+
res.on("close", () => {
|
|
177
|
+
transport.close();
|
|
178
|
+
server.close();
|
|
179
|
+
});
|
|
180
|
+
await server.connect(transport);
|
|
181
|
+
await transport.handleRequest(req, res, req.body);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (!res.headersSent) {
|
|
184
|
+
res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal error" }, id: null });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const methodNotAllowed = (_req, res) =>
|
|
190
|
+
res.status(405).json({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null });
|
|
191
|
+
app.get("/mcp", methodNotAllowed);
|
|
192
|
+
app.delete("/mcp", methodNotAllowed);
|
|
193
|
+
|
|
194
|
+
app.listen(PORT, "127.0.0.1", () => {
|
|
195
|
+
log(`ClickShot MCP server on http://127.0.0.1:${PORT}`);
|
|
196
|
+
log(` • extension ingest: POST http://127.0.0.1:${PORT}/capture`);
|
|
197
|
+
log(` • MCP endpoint: http://127.0.0.1:${PORT}/mcp`);
|
|
198
|
+
log(`Register in Claude Code:`);
|
|
199
|
+
log(` claude mcp add --transport http clickshot http://127.0.0.1:${PORT}/mcp`);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
main().catch((e) => {
|
|
205
|
+
log("Fatal:", e);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
});
|