clickshot-mcp 0.1.0 → 0.1.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/package.json +1 -1
- package/server.js +124 -17
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -18,7 +18,9 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
18
18
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
19
19
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
20
|
|
|
21
|
-
const VERSION = "0.1.
|
|
21
|
+
const VERSION = "0.1.2";
|
|
22
|
+
|
|
23
|
+
const WATCH_INTERVAL_MS = 4000; // periodic-frame cadence the extension uses while watching
|
|
22
24
|
|
|
23
25
|
// stdout is the JSON-RPC channel in stdio mode, so all logging goes to stderr.
|
|
24
26
|
const log = (...a) => console.error(...a);
|
|
@@ -52,6 +54,18 @@ const MAX_BUFFER = 200; // keep the most recent N captures in memory
|
|
|
52
54
|
const captures = [];
|
|
53
55
|
let nextId = 1;
|
|
54
56
|
|
|
57
|
+
// Claude-initiated "watch" session state. The extension polls GET /watch and
|
|
58
|
+
// starts recording (+ periodic frames) when `on` is true. Turned on by the
|
|
59
|
+
// start_watching tool (which the MCP client gates with a user approval prompt)
|
|
60
|
+
// and off by stop_watching or by the user via the extension.
|
|
61
|
+
const watch = { on: false, task: null, since: null };
|
|
62
|
+
|
|
63
|
+
function setWatch(on, task) {
|
|
64
|
+
watch.on = !!on;
|
|
65
|
+
watch.task = on ? task || null : null;
|
|
66
|
+
watch.since = on ? Date.now() : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
// ---------------------------------------------------------------------------
|
|
56
70
|
// MCP server definition (tools)
|
|
57
71
|
// ---------------------------------------------------------------------------
|
|
@@ -91,22 +105,26 @@ function buildMcpServer() {
|
|
|
91
105
|
{
|
|
92
106
|
type: "text",
|
|
93
107
|
text:
|
|
94
|
-
`Most recent ${recent.length} browser
|
|
95
|
-
`
|
|
108
|
+
`Most recent ${recent.length} browser frame(s), oldest first. ` +
|
|
109
|
+
`Click frames have a red marker at the click point; periodic frames ` +
|
|
110
|
+
`(captured while watching) do not.`,
|
|
96
111
|
},
|
|
97
112
|
];
|
|
98
113
|
|
|
99
114
|
for (const c of recent) {
|
|
100
115
|
const m = c.meta || {};
|
|
101
116
|
const when = m.timestamp ? new Date(m.timestamp).toISOString() : "unknown time";
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
let line;
|
|
118
|
+
if (m.kind === "interval") {
|
|
119
|
+
line = `#${c.id} ${when} — ${m.title || ""}\n ${m.url || ""}\n (periodic frame)`;
|
|
120
|
+
} else {
|
|
121
|
+
const what =
|
|
122
|
+
(m.tag ? `<${m.tag}>` : "element") +
|
|
123
|
+
(m.text ? ` "${m.text}"` : "") +
|
|
124
|
+
(m.selector ? ` [${m.selector}]` : "");
|
|
125
|
+
line = `#${c.id} ${when} — ${m.title || ""}\n ${m.url || ""}\n clicked ${what}`;
|
|
126
|
+
}
|
|
127
|
+
content.push({ type: "text", text: line });
|
|
110
128
|
content.push({ type: "image", data: c.image, mimeType: c.mimeType || "image/jpeg" });
|
|
111
129
|
}
|
|
112
130
|
|
|
@@ -114,6 +132,56 @@ function buildMcpServer() {
|
|
|
114
132
|
}
|
|
115
133
|
);
|
|
116
134
|
|
|
135
|
+
server.registerTool(
|
|
136
|
+
"start_watching",
|
|
137
|
+
{
|
|
138
|
+
title: "Watch the user perform a task",
|
|
139
|
+
description:
|
|
140
|
+
"Begin observing the user's browser so you can see how they perform a task you've proposed. " +
|
|
141
|
+
"Use this AFTER suggesting a concrete task and getting the user's go-ahead: it turns on " +
|
|
142
|
+
"recording in their ClickShot extension (with a visible on-screen 'Claude is watching' " +
|
|
143
|
+
"indicator) and captures their clicks plus periodic screenshots. When the user says they're " +
|
|
144
|
+
"done, call get_recent_activity to review how they did it, then stop_watching. " +
|
|
145
|
+
"The user is shown an approval prompt for this action and can stop watching at any time.",
|
|
146
|
+
inputSchema: {
|
|
147
|
+
task: z.string().describe("The task you are about to watch the user perform, e.g. 'file an expense report'."),
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
async ({ task }) => {
|
|
151
|
+
setWatch(true, task);
|
|
152
|
+
return {
|
|
153
|
+
content: [
|
|
154
|
+
{
|
|
155
|
+
type: "text",
|
|
156
|
+
text:
|
|
157
|
+
`Now watching for: "${task}".\n` +
|
|
158
|
+
`The ClickShot extension will start recording within a couple of seconds and show a ` +
|
|
159
|
+
`"Claude is watching" indicator. Ask the user to perform the task now. When they say ` +
|
|
160
|
+
`they're done, call get_recent_activity to review the steps, then stop_watching.`,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
server.registerTool(
|
|
168
|
+
"stop_watching",
|
|
169
|
+
{
|
|
170
|
+
title: "Stop watching",
|
|
171
|
+
description: "End the current watch session. The extension stops recording and hides the watching indicator.",
|
|
172
|
+
inputSchema: {},
|
|
173
|
+
},
|
|
174
|
+
async () => {
|
|
175
|
+
const was = watch.task;
|
|
176
|
+
setWatch(false);
|
|
177
|
+
return {
|
|
178
|
+
content: [
|
|
179
|
+
{ type: "text", text: was ? `Stopped watching "${was}".` : "Watching was not active." },
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
|
|
117
185
|
server.registerTool(
|
|
118
186
|
"clear_activity",
|
|
119
187
|
{
|
|
@@ -147,20 +215,51 @@ function buildIngestApp() {
|
|
|
147
215
|
res.json({ ok: true, buffered: captures.length });
|
|
148
216
|
});
|
|
149
217
|
|
|
150
|
-
app.get("/health", (_req, res) => res.json({ ok: true, captures: captures.length, mode: STDIO ? "stdio" : "http" }));
|
|
218
|
+
app.get("/health", (_req, res) => res.json({ ok: true, captures: captures.length, mode: STDIO ? "stdio" : "http", watching: watch.on }));
|
|
219
|
+
|
|
220
|
+
// The extension polls this to know whether Claude has asked to watch.
|
|
221
|
+
app.get("/watch", (_req, res) =>
|
|
222
|
+
res.json({ watching: watch.on, task: watch.task, since: watch.since, intervalMs: WATCH_INTERVAL_MS })
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Lets the extension (the user's Stop button) revoke a watch session.
|
|
226
|
+
app.post("/watch", (req, res) => {
|
|
227
|
+
const on = !!(req.body && req.body.watching);
|
|
228
|
+
setWatch(on, req.body && req.body.task);
|
|
229
|
+
res.json({ ok: true, watching: watch.on });
|
|
230
|
+
});
|
|
231
|
+
|
|
151
232
|
return app;
|
|
152
233
|
}
|
|
153
234
|
|
|
154
235
|
// ---------------------------------------------------------------------------
|
|
155
236
|
// Boot
|
|
156
237
|
// ---------------------------------------------------------------------------
|
|
238
|
+
// Bind the ingest port without letting a conflict crash the process.
|
|
239
|
+
function listenIngest(app, { fatalOnError }) {
|
|
240
|
+
const srv = app.listen(PORT, "127.0.0.1", () => {
|
|
241
|
+
log(`ClickShot ingest on http://127.0.0.1:${PORT}/capture`);
|
|
242
|
+
});
|
|
243
|
+
srv.on("error", (err) => {
|
|
244
|
+
if (err && err.code === "EADDRINUSE") {
|
|
245
|
+
if (fatalOnError) {
|
|
246
|
+
log(`Port ${PORT} is already in use — another ClickShot server is running. Stop it, or set CLICKSHOT_PORT to a free port.`);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
log(`Port ${PORT} already in use — another ClickShot server is handling ingest. Continuing with MCP over stdio.`);
|
|
250
|
+
} else {
|
|
251
|
+
log("Ingest server error:", err && err.message ? err.message : err);
|
|
252
|
+
if (fatalOnError) process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
return srv;
|
|
256
|
+
}
|
|
257
|
+
|
|
157
258
|
async function main() {
|
|
158
259
|
if (STDIO) {
|
|
159
260
|
// Claude Desktop launches this; talk MCP over stdio, ingest over HTTP.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
log(`ClickShot ingest on http://127.0.0.1:${PORT}/capture (stdio MCP mode)`);
|
|
163
|
-
});
|
|
261
|
+
// A port conflict must NOT kill the MCP connection.
|
|
262
|
+
listenIngest(buildIngestApp(), { fatalOnError: false });
|
|
164
263
|
const server = buildMcpServer();
|
|
165
264
|
const transport = new StdioServerTransport();
|
|
166
265
|
await server.connect(transport);
|
|
@@ -191,13 +290,21 @@ async function main() {
|
|
|
191
290
|
app.get("/mcp", methodNotAllowed);
|
|
192
291
|
app.delete("/mcp", methodNotAllowed);
|
|
193
292
|
|
|
194
|
-
app.listen(PORT, "127.0.0.1", () => {
|
|
293
|
+
const srv = app.listen(PORT, "127.0.0.1", () => {
|
|
195
294
|
log(`ClickShot MCP server on http://127.0.0.1:${PORT}`);
|
|
196
295
|
log(` • extension ingest: POST http://127.0.0.1:${PORT}/capture`);
|
|
197
296
|
log(` • MCP endpoint: http://127.0.0.1:${PORT}/mcp`);
|
|
198
297
|
log(`Register in Claude Code:`);
|
|
199
298
|
log(` claude mcp add --transport http clickshot http://127.0.0.1:${PORT}/mcp`);
|
|
200
299
|
});
|
|
300
|
+
srv.on("error", (err) => {
|
|
301
|
+
if (err && err.code === "EADDRINUSE") {
|
|
302
|
+
log(`Port ${PORT} is already in use — another ClickShot server is running. Stop it, or set CLICKSHOT_PORT to a free port.`);
|
|
303
|
+
} else {
|
|
304
|
+
log("Server error:", err && err.message ? err.message : err);
|
|
305
|
+
}
|
|
306
|
+
process.exit(1);
|
|
307
|
+
});
|
|
201
308
|
}
|
|
202
309
|
}
|
|
203
310
|
|