heroshot 0.18.0 → 0.19.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/dist/cli/cli.js +6 -1
- package/dist/mcp/index.js +4 -311
- package/dist/server-BklhdmjN.js +319 -0
- package/dist/server-CqRfIFJa.js +3 -0
- package/package.json +1 -1
package/dist/cli/cli.js
CHANGED
|
@@ -770,8 +770,9 @@ function getVersion() {
|
|
|
770
770
|
}
|
|
771
771
|
const version = getVersion();
|
|
772
772
|
const program = new Command();
|
|
773
|
-
program.name("heroshot").description("Define your screenshots once, update them forever with one command").version(version).option("-v, --verbose", "Show detailed output").option("-c, --config <path>", "Path to config file").option("-s, --session-key <key>", "Session key for encrypted auth (or set HEROSHOT_SESSION_KEY)").hook("preAction", () => {
|
|
773
|
+
program.name("heroshot").description("Define your screenshots once, update them forever with one command").version(version).option("-v, --verbose", "Show detailed output").option("-c, --config <path>", "Path to config file").option("-s, --session-key <key>", "Session key for encrypted auth (or set HEROSHOT_SESSION_KEY)").hook("preAction", (_thisCommand, actionCommand) => {
|
|
774
774
|
setVerbose(program.opts().verbose ?? false);
|
|
775
|
+
if (actionCommand.name() === "mcp") return;
|
|
775
776
|
intro(version);
|
|
776
777
|
});
|
|
777
778
|
program.command("oneshot [url]", { isDefault: true }).description("Capture URL directly, or sync all screenshots from config").option("--selector <selector...>", "CSS selector(s) to capture").option("-o, --output <file>", "Output filename").option("-p, --padding <pixels>", "Padding around element", Number.parseInt).option("-w, --width <pixels>", "Viewport width", Number.parseInt).option("--height <pixels>", "Viewport height", Number.parseInt).option("--mobile", "Use mobile viewport (430x932)").option("--tablet", "Use tablet viewport (768x1024)").option("--desktop", "Use desktop viewport (1280x800)").option("--dark", "Force dark color scheme").option("--light", "Force light color scheme").option("--scale <factor>", "Device scale factor (1, 2, 3)", Number.parseInt).option("--retina", "Use retina scale (2x)").option("-q, --quality <percent>", "JPEG quality (1-100), outputs JPEG", Number.parseInt).option("--viewport-only", "Capture only viewport (not full page)").option("--reduced-motion", "Emulate prefers-reduced-motion: reduce (disables animations)").option("--user-agent <string>", "Custom user agent string").option("--ignore-https-errors", "Ignore TLS certificate errors (self-signed certs, dev only)").option("--save", "Save screenshot definition to config").option("--clean", "Delete stale files in output directory").option("--workers <count>", "Number of parallel capture workers", Number.parseInt).option("--headed", "Run browser in headed mode (visible window) for debugging").action(async (url, options) => {
|
|
@@ -787,6 +788,10 @@ program.command("snippet [pattern]").description("Generate markdown/HTML snippet
|
|
|
787
788
|
const globalOptions = program.opts();
|
|
788
789
|
if (!snippetAction(pattern, options ?? {}, globalOptions.config)) process.exitCode = 1;
|
|
789
790
|
});
|
|
791
|
+
program.command("mcp").description("Start the MCP (Model Context Protocol) server over stdio").action(async () => {
|
|
792
|
+
const { startMcpServer } = await import("../server-CqRfIFJa.js");
|
|
793
|
+
await startMcpServer();
|
|
794
|
+
});
|
|
790
795
|
program.command("list").description("List all configured screenshots").option("--json", "Output as JSON").action((options) => {
|
|
791
796
|
if (!listAction(options, program.opts())) process.exitCode = 1;
|
|
792
797
|
});
|
package/dist/mcp/index.js
CHANGED
|
@@ -1,317 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
-
//#region src/mcp/schemas/add.ts
|
|
7
|
-
/**
|
|
8
|
-
* Add tool input schema.
|
|
9
|
-
* Wraps the existing screenshotSchema from heroshot.
|
|
10
|
-
*/
|
|
11
|
-
const screenshotInputSchema = screenshotSchema.omit({ id: true });
|
|
12
|
-
const addOptionsSchema = z.object({
|
|
13
|
-
configPath: z.string().optional().describe("Path to config file (default: .heroshot/config.json in cwd)"),
|
|
14
|
-
screenshot: screenshotInputSchema.describe("Screenshot definition to add")
|
|
15
|
-
});
|
|
16
|
-
//#endregion
|
|
17
|
-
//#region src/mcp/schemas/list.ts
|
|
18
|
-
/**
|
|
19
|
-
* List tool input schema.
|
|
20
|
-
*/
|
|
21
|
-
const listOptionsSchema = z.object({
|
|
22
|
-
configPath: z.string().optional().describe("Path to config file (default: .heroshot/config.json in cwd)"),
|
|
23
|
-
filter: z.string().optional().describe("Filter screenshots by id, name, or filename (case-insensitive substring match)")
|
|
24
|
-
});
|
|
25
|
-
//#endregion
|
|
26
|
-
//#region src/mcp/schemas/remove.ts
|
|
27
|
-
/**
|
|
28
|
-
* Remove tool input schema.
|
|
29
|
-
*/
|
|
30
|
-
const removeOptionsSchema = z.object({
|
|
31
|
-
configPath: z.string().optional().describe("Path to config file (default: .heroshot/config.json in cwd)"),
|
|
32
|
-
id: z.string().min(1).describe("ID of the screenshot to remove")
|
|
33
|
-
});
|
|
34
|
-
//#endregion
|
|
35
|
-
//#region src/mcp/schemas/snippet.ts
|
|
36
|
-
/**
|
|
37
|
-
* Snippet tool input schema.
|
|
38
|
-
*/
|
|
39
|
-
const snippetOptionsSchema = z.object({
|
|
40
|
-
configPath: z.string().optional().describe("Path to config file (default: .heroshot/config.json in cwd)"),
|
|
41
|
-
filter: z.string().optional().describe("Filter screenshots by id, name, or filename (case-insensitive substring match)"),
|
|
42
|
-
pathPrefix: z.string().optional().describe("Custom path prefix for images (default: ./heroshots/)")
|
|
43
|
-
});
|
|
44
|
-
//#endregion
|
|
45
|
-
//#region src/mcp/schemas/sync.ts
|
|
46
|
-
/**
|
|
47
|
-
* Sync tool input schema.
|
|
48
|
-
*/
|
|
49
|
-
const syncOptionsSchema = z.object({
|
|
50
|
-
configPath: z.string().optional().describe("Path to config file (default: .heroshot/config.json in cwd)"),
|
|
51
|
-
filter: z.string().optional().describe("Filter screenshots by id, name, or filename (case-insensitive substring match)"),
|
|
52
|
-
clean: z.boolean().optional().describe("Delete stale files in output directory"),
|
|
53
|
-
workers: z.number().int().min(1).optional().describe("Number of parallel capture workers"),
|
|
54
|
-
sessionKey: z.string().optional().describe("Encrypted session key for authenticated screenshots")
|
|
55
|
-
});
|
|
56
|
-
//#endregion
|
|
57
|
-
//#region src/mcp/tools/add.ts
|
|
58
|
-
/**
|
|
59
|
-
* Add tool handler.
|
|
60
|
-
* Adds a new screenshot definition to config.
|
|
61
|
-
*/
|
|
62
|
-
function addHandler(input) {
|
|
63
|
-
try {
|
|
64
|
-
const configPath = input.configPath ?? getConfigPath();
|
|
65
|
-
const config = loadConfig(configPath);
|
|
66
|
-
const screenshotInput = {
|
|
67
|
-
...input.screenshot,
|
|
68
|
-
id: generateUid()
|
|
69
|
-
};
|
|
70
|
-
const screenshot = screenshotSchema.parse(screenshotInput);
|
|
71
|
-
const existingByName = config.screenshots.find((s) => s.name.toLowerCase() === screenshot.name.toLowerCase());
|
|
72
|
-
if (existingByName) return {
|
|
73
|
-
success: false,
|
|
74
|
-
error: `Screenshot with name "${screenshot.name}" already exists (id: ${existingByName.id})`
|
|
75
|
-
};
|
|
76
|
-
config.screenshots.push(screenshot);
|
|
77
|
-
saveConfig(configPath, config);
|
|
78
|
-
return {
|
|
79
|
-
success: true,
|
|
80
|
-
id: screenshot.id,
|
|
81
|
-
screenshot: {
|
|
82
|
-
id: screenshot.id,
|
|
83
|
-
name: screenshot.name,
|
|
84
|
-
url: screenshot.url
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
} catch (error) {
|
|
88
|
-
return {
|
|
89
|
-
success: false,
|
|
90
|
-
error: error instanceof Error ? error.message : String(error)
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
//#endregion
|
|
95
|
-
//#region src/mcp/tools/list.ts
|
|
96
|
-
/**
|
|
97
|
-
* List tool handler.
|
|
98
|
-
* Lists screenshots defined in config.
|
|
99
|
-
*/
|
|
100
|
-
function listHandler(input) {
|
|
101
|
-
try {
|
|
102
|
-
const screenshots = filterScreenshots(loadConfig(input.configPath ?? getConfigPath()).screenshots, input.filter);
|
|
103
|
-
return {
|
|
104
|
-
success: true,
|
|
105
|
-
count: screenshots.length,
|
|
106
|
-
screenshots: screenshots.map((s) => ({
|
|
107
|
-
id: s.id,
|
|
108
|
-
name: s.name,
|
|
109
|
-
url: s.url,
|
|
110
|
-
selector: s.selector
|
|
111
|
-
}))
|
|
112
|
-
};
|
|
113
|
-
} catch {
|
|
114
|
-
return {
|
|
115
|
-
success: false,
|
|
116
|
-
count: 0,
|
|
117
|
-
screenshots: [],
|
|
118
|
-
error: "Failed to load config"
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
//#endregion
|
|
123
|
-
//#region src/mcp/tools/remove.ts
|
|
124
|
-
/**
|
|
125
|
-
* Remove tool handler.
|
|
126
|
-
* Removes a screenshot definition from config.
|
|
127
|
-
*/
|
|
128
|
-
function removeHandler(input) {
|
|
129
|
-
try {
|
|
130
|
-
const configPath = input.configPath ?? getConfigPath();
|
|
131
|
-
const config = loadConfig(configPath);
|
|
132
|
-
const index = config.screenshots.findIndex((s) => s.id === input.id);
|
|
133
|
-
if (index === -1) return {
|
|
134
|
-
success: false,
|
|
135
|
-
error: `Screenshot with id "${input.id}" not found`
|
|
136
|
-
};
|
|
137
|
-
const [removed] = config.screenshots.splice(index, 1);
|
|
138
|
-
if (!removed) return {
|
|
139
|
-
success: false,
|
|
140
|
-
error: `Screenshot with id "${input.id}" not found`
|
|
141
|
-
};
|
|
142
|
-
saveConfig(configPath, config);
|
|
143
|
-
return {
|
|
144
|
-
success: true,
|
|
145
|
-
removed: {
|
|
146
|
-
id: removed.id,
|
|
147
|
-
name: removed.name
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
} catch (error) {
|
|
151
|
-
return {
|
|
152
|
-
success: false,
|
|
153
|
-
error: error instanceof Error ? error.message : String(error)
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
//#endregion
|
|
158
|
-
//#region src/mcp/tools/snippet.ts
|
|
159
|
-
/**
|
|
160
|
-
* Snippet tool handler.
|
|
161
|
-
* Generates markdown/HTML snippets for screenshots.
|
|
162
|
-
*/
|
|
163
|
-
function snippetHandler(input) {
|
|
164
|
-
try {
|
|
165
|
-
const results = generateSnippets(loadConfig(input.configPath ?? getConfigPath()), input.filter, { pathPrefix: input.pathPrefix });
|
|
166
|
-
return {
|
|
167
|
-
success: true,
|
|
168
|
-
count: results.length,
|
|
169
|
-
snippets: results.map((r) => ({
|
|
170
|
-
id: r.screenshot.id,
|
|
171
|
-
name: r.screenshot.name,
|
|
172
|
-
snippet: r.snippet
|
|
173
|
-
}))
|
|
174
|
-
};
|
|
175
|
-
} catch (error) {
|
|
176
|
-
return {
|
|
177
|
-
success: false,
|
|
178
|
-
count: 0,
|
|
179
|
-
snippets: [],
|
|
180
|
-
error: error instanceof Error ? error.message : String(error)
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
//#endregion
|
|
185
|
-
//#region src/mcp/tools/sync.ts
|
|
186
|
-
/**
|
|
187
|
-
* Sync tool handler.
|
|
188
|
-
* Captures screenshots defined in config.
|
|
189
|
-
*/
|
|
190
|
-
function resolveSessionKey(input) {
|
|
191
|
-
return input.sessionKey ?? process.env["HEROSHOT_SESSION_KEY"];
|
|
192
|
-
}
|
|
193
|
-
async function syncHandler(input) {
|
|
194
|
-
try {
|
|
195
|
-
const configPath = input.configPath ?? getConfigPath();
|
|
196
|
-
const sessionKey = resolveSessionKey(input);
|
|
197
|
-
const result = await sync({
|
|
198
|
-
configPath,
|
|
199
|
-
filter: input.filter,
|
|
200
|
-
clean: input.clean,
|
|
201
|
-
workers: input.workers,
|
|
202
|
-
sessionKey
|
|
203
|
-
});
|
|
204
|
-
return {
|
|
205
|
-
success: result.failed === 0,
|
|
206
|
-
total: result.total,
|
|
207
|
-
captured: result.success,
|
|
208
|
-
failed: result.failed,
|
|
209
|
-
results: result.results,
|
|
210
|
-
staleFiles: result.staleFiles,
|
|
211
|
-
deletedFiles: result.deletedFiles
|
|
212
|
-
};
|
|
213
|
-
} catch (error) {
|
|
214
|
-
return {
|
|
215
|
-
success: false,
|
|
216
|
-
total: 0,
|
|
217
|
-
captured: 0,
|
|
218
|
-
failed: 0,
|
|
219
|
-
results: [],
|
|
220
|
-
error: error instanceof Error ? error.message : String(error)
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
//#endregion
|
|
225
|
-
//#region src/mcp/tools/definitions.ts
|
|
226
|
-
function formatResult(result) {
|
|
227
|
-
return {
|
|
228
|
-
content: [{
|
|
229
|
-
type: "text",
|
|
230
|
-
text: JSON.stringify(result, null, 2)
|
|
231
|
-
}],
|
|
232
|
-
isError: !result.success
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
async function syncToolHandler(input) {
|
|
236
|
-
return formatResult(await syncHandler(input));
|
|
237
|
-
}
|
|
238
|
-
async function addToolHandler(input) {
|
|
239
|
-
return formatResult(addHandler(input));
|
|
240
|
-
}
|
|
241
|
-
async function listToolHandler(input) {
|
|
242
|
-
return formatResult(listHandler(input));
|
|
243
|
-
}
|
|
244
|
-
async function snippetToolHandler(input) {
|
|
245
|
-
return formatResult(snippetHandler(input));
|
|
246
|
-
}
|
|
247
|
-
async function removeToolHandler(input) {
|
|
248
|
-
return formatResult(removeHandler(input));
|
|
249
|
-
}
|
|
250
|
-
const tools = [
|
|
251
|
-
{
|
|
252
|
-
name: "heroshot_sync",
|
|
253
|
-
description: "Capture all screenshots defined in the heroshot config. Optionally filter by id/name/filename pattern, delete stale files, or run with multiple workers.",
|
|
254
|
-
inputSchema: syncOptionsSchema,
|
|
255
|
-
handler: syncToolHandler
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
name: "heroshot_add",
|
|
259
|
-
description: "Add a new screenshot definition to the heroshot config. Requires at least name and url. Optional: selector for element capture, actions for pre-capture steps.",
|
|
260
|
-
inputSchema: addOptionsSchema,
|
|
261
|
-
handler: addToolHandler
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
name: "heroshot_list",
|
|
265
|
-
description: "List all screenshots defined in the heroshot config. Returns id, name, url, and selector for each screenshot.",
|
|
266
|
-
inputSchema: listOptionsSchema,
|
|
267
|
-
handler: listToolHandler
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
name: "heroshot_snippet",
|
|
271
|
-
description: "Generate markdown/HTML snippets for embedding screenshots in documentation. Supports light/dark mode with <picture> elements.",
|
|
272
|
-
inputSchema: snippetOptionsSchema,
|
|
273
|
-
handler: snippetToolHandler
|
|
274
|
-
},
|
|
275
|
-
{
|
|
276
|
-
name: "heroshot_remove",
|
|
277
|
-
description: "Remove a screenshot definition from the heroshot config by its ID.",
|
|
278
|
-
inputSchema: removeOptionsSchema,
|
|
279
|
-
handler: removeToolHandler
|
|
280
|
-
}
|
|
281
|
-
];
|
|
282
|
-
//#endregion
|
|
283
|
-
//#region src/mcp/utils/zodToMcp.ts
|
|
284
|
-
/**
|
|
285
|
-
* Utilities for converting Zod schemas to MCP tool definitions.
|
|
286
|
-
*/
|
|
287
|
-
/**
|
|
288
|
-
* Convert a ToolDefinition to MCP tool format.
|
|
289
|
-
* Uses z.toJSONSchema() for automatic schema conversion.
|
|
290
|
-
*/
|
|
291
|
-
function toMcpTool(definition) {
|
|
292
|
-
return {
|
|
293
|
-
name: definition.name,
|
|
294
|
-
description: definition.description,
|
|
295
|
-
inputSchema: z.toJSONSchema(definition.inputSchema)
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
//#endregion
|
|
2
|
+
import { t as startMcpServer } from "../server-BklhdmjN.js";
|
|
299
3
|
//#region src/mcp/index.ts
|
|
300
4
|
/**
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
* MCP server for screenshot automation. Tools are auto-derived from Zod schemas
|
|
304
|
-
* using z.toJSONSchema() for low maintenance.
|
|
5
|
+
* Entry point for the `heroshot-mcp` bin.
|
|
6
|
+
* Also reachable via the `heroshot mcp` subcommand (see src/cli/cli.ts).
|
|
305
7
|
*/
|
|
306
|
-
|
|
307
|
-
name: "heroshot",
|
|
308
|
-
version: "0.1.0"
|
|
309
|
-
});
|
|
310
|
-
for (const tool of tools) {
|
|
311
|
-
const mcpTool = toMcpTool(tool);
|
|
312
|
-
server.registerTool(tool.name, mcpTool.inputSchema, async (input) => tool.handler(input));
|
|
313
|
-
}
|
|
314
|
-
const transport = new StdioServerTransport();
|
|
315
|
-
await server.connect(transport);
|
|
8
|
+
await startMcpServer();
|
|
316
9
|
//#endregion
|
|
317
10
|
export {};
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { _ as loadConfig, b as screenshotSchema, g as getConfigPath, i as filterScreenshots, r as sync, t as generateSnippets, v as saveConfig, x as generateUid } from "./snippet-ZcZmuryj.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
//#region src/mcp/schemas/add.ts
|
|
7
|
+
/**
|
|
8
|
+
* Add tool input schema.
|
|
9
|
+
* Wraps the existing screenshotSchema from heroshot.
|
|
10
|
+
*/
|
|
11
|
+
const screenshotInputSchema = screenshotSchema.omit({ id: true });
|
|
12
|
+
const addOptionsSchema = z.object({
|
|
13
|
+
configPath: z.string().optional().describe("Path to config file (default: .heroshot/config.json in cwd)"),
|
|
14
|
+
screenshot: screenshotInputSchema.describe("Screenshot definition to add")
|
|
15
|
+
});
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/mcp/schemas/list.ts
|
|
18
|
+
/**
|
|
19
|
+
* List tool input schema.
|
|
20
|
+
*/
|
|
21
|
+
const listOptionsSchema = z.object({
|
|
22
|
+
configPath: z.string().optional().describe("Path to config file (default: .heroshot/config.json in cwd)"),
|
|
23
|
+
filter: z.string().optional().describe("Filter screenshots by id, name, or filename (case-insensitive substring match)")
|
|
24
|
+
});
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/mcp/schemas/remove.ts
|
|
27
|
+
/**
|
|
28
|
+
* Remove tool input schema.
|
|
29
|
+
*/
|
|
30
|
+
const removeOptionsSchema = z.object({
|
|
31
|
+
configPath: z.string().optional().describe("Path to config file (default: .heroshot/config.json in cwd)"),
|
|
32
|
+
id: z.string().min(1).describe("ID of the screenshot to remove")
|
|
33
|
+
});
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/mcp/schemas/snippet.ts
|
|
36
|
+
/**
|
|
37
|
+
* Snippet tool input schema.
|
|
38
|
+
*/
|
|
39
|
+
const snippetOptionsSchema = z.object({
|
|
40
|
+
configPath: z.string().optional().describe("Path to config file (default: .heroshot/config.json in cwd)"),
|
|
41
|
+
filter: z.string().optional().describe("Filter screenshots by id, name, or filename (case-insensitive substring match)"),
|
|
42
|
+
pathPrefix: z.string().optional().describe("Custom path prefix for images (default: ./heroshots/)")
|
|
43
|
+
});
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/mcp/schemas/sync.ts
|
|
46
|
+
/**
|
|
47
|
+
* Sync tool input schema.
|
|
48
|
+
*/
|
|
49
|
+
const syncOptionsSchema = z.object({
|
|
50
|
+
configPath: z.string().optional().describe("Path to config file (default: .heroshot/config.json in cwd)"),
|
|
51
|
+
filter: z.string().optional().describe("Filter screenshots by id, name, or filename (case-insensitive substring match)"),
|
|
52
|
+
clean: z.boolean().optional().describe("Delete stale files in output directory"),
|
|
53
|
+
workers: z.number().int().min(1).optional().describe("Number of parallel capture workers"),
|
|
54
|
+
sessionKey: z.string().optional().describe("Encrypted session key for authenticated screenshots")
|
|
55
|
+
});
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/mcp/tools/add.ts
|
|
58
|
+
/**
|
|
59
|
+
* Add tool handler.
|
|
60
|
+
* Adds a new screenshot definition to config.
|
|
61
|
+
*/
|
|
62
|
+
function addHandler(input) {
|
|
63
|
+
try {
|
|
64
|
+
const configPath = input.configPath ?? getConfigPath();
|
|
65
|
+
const config = loadConfig(configPath);
|
|
66
|
+
const screenshotInput = {
|
|
67
|
+
...input.screenshot,
|
|
68
|
+
id: generateUid()
|
|
69
|
+
};
|
|
70
|
+
const screenshot = screenshotSchema.parse(screenshotInput);
|
|
71
|
+
const existingByName = config.screenshots.find((s) => s.name.toLowerCase() === screenshot.name.toLowerCase());
|
|
72
|
+
if (existingByName) return {
|
|
73
|
+
success: false,
|
|
74
|
+
error: `Screenshot with name "${screenshot.name}" already exists (id: ${existingByName.id})`
|
|
75
|
+
};
|
|
76
|
+
config.screenshots.push(screenshot);
|
|
77
|
+
saveConfig(configPath, config);
|
|
78
|
+
return {
|
|
79
|
+
success: true,
|
|
80
|
+
id: screenshot.id,
|
|
81
|
+
screenshot: {
|
|
82
|
+
id: screenshot.id,
|
|
83
|
+
name: screenshot.name,
|
|
84
|
+
url: screenshot.url
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
error: error instanceof Error ? error.message : String(error)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/mcp/tools/list.ts
|
|
96
|
+
/**
|
|
97
|
+
* List tool handler.
|
|
98
|
+
* Lists screenshots defined in config.
|
|
99
|
+
*/
|
|
100
|
+
function listHandler(input) {
|
|
101
|
+
try {
|
|
102
|
+
const screenshots = filterScreenshots(loadConfig(input.configPath ?? getConfigPath()).screenshots, input.filter);
|
|
103
|
+
return {
|
|
104
|
+
success: true,
|
|
105
|
+
count: screenshots.length,
|
|
106
|
+
screenshots: screenshots.map((s) => ({
|
|
107
|
+
id: s.id,
|
|
108
|
+
name: s.name,
|
|
109
|
+
url: s.url,
|
|
110
|
+
selector: s.selector
|
|
111
|
+
}))
|
|
112
|
+
};
|
|
113
|
+
} catch {
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
count: 0,
|
|
117
|
+
screenshots: [],
|
|
118
|
+
error: "Failed to load config"
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/mcp/tools/remove.ts
|
|
124
|
+
/**
|
|
125
|
+
* Remove tool handler.
|
|
126
|
+
* Removes a screenshot definition from config.
|
|
127
|
+
*/
|
|
128
|
+
function removeHandler(input) {
|
|
129
|
+
try {
|
|
130
|
+
const configPath = input.configPath ?? getConfigPath();
|
|
131
|
+
const config = loadConfig(configPath);
|
|
132
|
+
const index = config.screenshots.findIndex((s) => s.id === input.id);
|
|
133
|
+
if (index === -1) return {
|
|
134
|
+
success: false,
|
|
135
|
+
error: `Screenshot with id "${input.id}" not found`
|
|
136
|
+
};
|
|
137
|
+
const [removed] = config.screenshots.splice(index, 1);
|
|
138
|
+
if (!removed) return {
|
|
139
|
+
success: false,
|
|
140
|
+
error: `Screenshot with id "${input.id}" not found`
|
|
141
|
+
};
|
|
142
|
+
saveConfig(configPath, config);
|
|
143
|
+
return {
|
|
144
|
+
success: true,
|
|
145
|
+
removed: {
|
|
146
|
+
id: removed.id,
|
|
147
|
+
name: removed.name
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return {
|
|
152
|
+
success: false,
|
|
153
|
+
error: error instanceof Error ? error.message : String(error)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/mcp/tools/snippet.ts
|
|
159
|
+
/**
|
|
160
|
+
* Snippet tool handler.
|
|
161
|
+
* Generates markdown/HTML snippets for screenshots.
|
|
162
|
+
*/
|
|
163
|
+
function snippetHandler(input) {
|
|
164
|
+
try {
|
|
165
|
+
const results = generateSnippets(loadConfig(input.configPath ?? getConfigPath()), input.filter, { pathPrefix: input.pathPrefix });
|
|
166
|
+
return {
|
|
167
|
+
success: true,
|
|
168
|
+
count: results.length,
|
|
169
|
+
snippets: results.map((r) => ({
|
|
170
|
+
id: r.screenshot.id,
|
|
171
|
+
name: r.screenshot.name,
|
|
172
|
+
snippet: r.snippet
|
|
173
|
+
}))
|
|
174
|
+
};
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return {
|
|
177
|
+
success: false,
|
|
178
|
+
count: 0,
|
|
179
|
+
snippets: [],
|
|
180
|
+
error: error instanceof Error ? error.message : String(error)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/mcp/tools/sync.ts
|
|
186
|
+
/**
|
|
187
|
+
* Sync tool handler.
|
|
188
|
+
* Captures screenshots defined in config.
|
|
189
|
+
*/
|
|
190
|
+
function resolveSessionKey(input) {
|
|
191
|
+
return input.sessionKey ?? process.env["HEROSHOT_SESSION_KEY"];
|
|
192
|
+
}
|
|
193
|
+
async function syncHandler(input) {
|
|
194
|
+
try {
|
|
195
|
+
const configPath = input.configPath ?? getConfigPath();
|
|
196
|
+
const sessionKey = resolveSessionKey(input);
|
|
197
|
+
const result = await sync({
|
|
198
|
+
configPath,
|
|
199
|
+
filter: input.filter,
|
|
200
|
+
clean: input.clean,
|
|
201
|
+
workers: input.workers,
|
|
202
|
+
sessionKey
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
success: result.failed === 0,
|
|
206
|
+
total: result.total,
|
|
207
|
+
captured: result.success,
|
|
208
|
+
failed: result.failed,
|
|
209
|
+
results: result.results,
|
|
210
|
+
staleFiles: result.staleFiles,
|
|
211
|
+
deletedFiles: result.deletedFiles
|
|
212
|
+
};
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return {
|
|
215
|
+
success: false,
|
|
216
|
+
total: 0,
|
|
217
|
+
captured: 0,
|
|
218
|
+
failed: 0,
|
|
219
|
+
results: [],
|
|
220
|
+
error: error instanceof Error ? error.message : String(error)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/mcp/tools/definitions.ts
|
|
226
|
+
function formatResult(result) {
|
|
227
|
+
return {
|
|
228
|
+
content: [{
|
|
229
|
+
type: "text",
|
|
230
|
+
text: JSON.stringify(result, null, 2)
|
|
231
|
+
}],
|
|
232
|
+
isError: !result.success
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
async function syncToolHandler(input) {
|
|
236
|
+
return formatResult(await syncHandler(input));
|
|
237
|
+
}
|
|
238
|
+
async function addToolHandler(input) {
|
|
239
|
+
return formatResult(addHandler(input));
|
|
240
|
+
}
|
|
241
|
+
async function listToolHandler(input) {
|
|
242
|
+
return formatResult(listHandler(input));
|
|
243
|
+
}
|
|
244
|
+
async function snippetToolHandler(input) {
|
|
245
|
+
return formatResult(snippetHandler(input));
|
|
246
|
+
}
|
|
247
|
+
async function removeToolHandler(input) {
|
|
248
|
+
return formatResult(removeHandler(input));
|
|
249
|
+
}
|
|
250
|
+
const tools = [
|
|
251
|
+
{
|
|
252
|
+
name: "heroshot_sync",
|
|
253
|
+
description: "Capture all screenshots defined in the heroshot config. Optionally filter by id/name/filename pattern, delete stale files, or run with multiple workers.",
|
|
254
|
+
inputSchema: syncOptionsSchema,
|
|
255
|
+
handler: syncToolHandler
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: "heroshot_add",
|
|
259
|
+
description: "Add a new screenshot definition to the heroshot config. Requires at least name and url. Optional: selector for element capture, actions for pre-capture steps.",
|
|
260
|
+
inputSchema: addOptionsSchema,
|
|
261
|
+
handler: addToolHandler
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "heroshot_list",
|
|
265
|
+
description: "List all screenshots defined in the heroshot config. Returns id, name, url, and selector for each screenshot.",
|
|
266
|
+
inputSchema: listOptionsSchema,
|
|
267
|
+
handler: listToolHandler
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: "heroshot_snippet",
|
|
271
|
+
description: "Generate markdown/HTML snippets for embedding screenshots in documentation. Supports light/dark mode with <picture> elements.",
|
|
272
|
+
inputSchema: snippetOptionsSchema,
|
|
273
|
+
handler: snippetToolHandler
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: "heroshot_remove",
|
|
277
|
+
description: "Remove a screenshot definition from the heroshot config by its ID.",
|
|
278
|
+
inputSchema: removeOptionsSchema,
|
|
279
|
+
handler: removeToolHandler
|
|
280
|
+
}
|
|
281
|
+
];
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/mcp/utils/zodToMcp.ts
|
|
284
|
+
/**
|
|
285
|
+
* Utilities for converting Zod schemas to MCP tool definitions.
|
|
286
|
+
*/
|
|
287
|
+
/**
|
|
288
|
+
* Convert a ToolDefinition to MCP tool format.
|
|
289
|
+
* Uses z.toJSONSchema() for automatic schema conversion.
|
|
290
|
+
*/
|
|
291
|
+
function toMcpTool(definition) {
|
|
292
|
+
return {
|
|
293
|
+
name: definition.name,
|
|
294
|
+
description: definition.description,
|
|
295
|
+
inputSchema: z.toJSONSchema(definition.inputSchema)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/mcp/server.ts
|
|
300
|
+
/**
|
|
301
|
+
* Heroshot MCP Server
|
|
302
|
+
*
|
|
303
|
+
* MCP server for screenshot automation. Tools are auto-derived from Zod schemas
|
|
304
|
+
* using z.toJSONSchema() for low maintenance.
|
|
305
|
+
*/
|
|
306
|
+
async function startMcpServer() {
|
|
307
|
+
const server = new McpServer({
|
|
308
|
+
name: "heroshot",
|
|
309
|
+
version: "0.1.0"
|
|
310
|
+
});
|
|
311
|
+
for (const tool of tools) {
|
|
312
|
+
const mcpTool = toMcpTool(tool);
|
|
313
|
+
server.registerTool(tool.name, mcpTool.inputSchema, async (input) => tool.handler(input));
|
|
314
|
+
}
|
|
315
|
+
const transport = new StdioServerTransport();
|
|
316
|
+
await server.connect(transport);
|
|
317
|
+
}
|
|
318
|
+
//#endregion
|
|
319
|
+
export { startMcpServer as t };
|