@dgr_labs/bezelbub-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/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/cli.js +111 -0
- package/dist/index.js +223 -0
- package/manifest.json +70 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DGR Labs, LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# @dgr_labs/bezelbub-mcp
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for [Bezelbub](https://dgrlabs.co): frame
|
|
4
|
+
screenshots and screen recordings in realistic Apple device bezels — iPhone,
|
|
5
|
+
iPad, MacBook, iMac, and Apple TV device mockups — straight from an AI
|
|
6
|
+
assistant. Supports transparent HEVC-with-alpha video export (with an optional
|
|
7
|
+
VP9/WebM copy for Chrome/Firefox).
|
|
8
|
+
|
|
9
|
+
The server is a thin stdio adapter over the `bezelbub` CLI: every tool shells
|
|
10
|
+
out to the CLI in `--json` mode, and the CLI's exit codes and suggestion-rich
|
|
11
|
+
stderr are surfaced as MCP tool errors.
|
|
12
|
+
|
|
13
|
+
> **macOS only.** The Bezelbub engine is a macOS-native binary (Core Graphics /
|
|
14
|
+
> AVFoundation), so this server runs only on a Mac. The npm package declares
|
|
15
|
+
> `"os": ["darwin"]` and will refuse to install elsewhere.
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
- macOS 14+
|
|
20
|
+
- Node.js 18+
|
|
21
|
+
- The `bezelbub` CLI:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
brew install cwooddgr/tap/bezelbub
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The server finds the CLI via the `BEZELBUB_CLI_PATH` environment variable if
|
|
28
|
+
set, otherwise on `PATH` (plus the standard Homebrew locations
|
|
29
|
+
`/opt/homebrew/bin` and `/usr/local/bin`, since GUI apps launch MCP servers
|
|
30
|
+
with a minimal `PATH`).
|
|
31
|
+
|
|
32
|
+
## Setup
|
|
33
|
+
|
|
34
|
+
### Claude Code
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
claude mcp add bezelbub -- npx -y @dgr_labs/bezelbub-mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Claude Desktop
|
|
41
|
+
|
|
42
|
+
Open the config file (Claude menu › Settings… › Developer › Edit Config), which
|
|
43
|
+
lives at `~/Library/Application Support/Claude/claude_desktop_config.json`, and
|
|
44
|
+
add:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"bezelbub": {
|
|
50
|
+
"command": "npx",
|
|
51
|
+
"args": ["-y", "@dgr_labs/bezelbub-mcp"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then restart Claude Desktop.
|
|
58
|
+
|
|
59
|
+
### Desktop Extension (.mcpb)
|
|
60
|
+
|
|
61
|
+
The repo includes a `manifest.json` conforming to the
|
|
62
|
+
[MCPB](https://github.com/modelcontextprotocol/mcpb) spec. To build a
|
|
63
|
+
one-click-installable bundle:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
npm ci && npm run build
|
|
67
|
+
npx @anthropic-ai/mcpb pack
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Tools
|
|
71
|
+
|
|
72
|
+
### `frame_image`
|
|
73
|
+
|
|
74
|
+
Frame a screenshot in a device bezel; writes a framed PNG.
|
|
75
|
+
|
|
76
|
+
| Parameter | Required | Description |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `input_path` | yes | Absolute path to a PNG/JPEG/HEIC screenshot |
|
|
79
|
+
| `device` | no | Device id (e.g. `iphone17pro`); auto-detected from pixel size when omitted |
|
|
80
|
+
| `color` | no | Bezel color name (e.g. `Cosmic Orange`); device default when omitted |
|
|
81
|
+
| `orientation` | no | `portrait` \| `landscape` \| `auto` (default) |
|
|
82
|
+
| `background` | no | Hex color (`#FFFFFF`, `#RRGGBBAA`) or `transparent` (default) |
|
|
83
|
+
| `output_size` | no | Width (`1920`), exact size (`1920x988`), or percentage (`50%`) |
|
|
84
|
+
| `output_path` | no | Defaults to `<input>-framed.png` beside the input |
|
|
85
|
+
|
|
86
|
+
Returns JSON: `{kind: "image", device, color, orientation, output, width, height}`.
|
|
87
|
+
|
|
88
|
+
### `frame_video`
|
|
89
|
+
|
|
90
|
+
Frame a screen recording (`.mov`/`.mp4`/`.m4v`) in a device bezel; writes a
|
|
91
|
+
framed MP4 with audio preserved. `background: "transparent"` switches the
|
|
92
|
+
export to HEVC-with-alpha in a QuickTime `.mov` (plays in Safari and Apple
|
|
93
|
+
frameworks only); `webm: true` additionally writes a VP9/WebM copy for
|
|
94
|
+
Chrome/Firefox (requires `ffmpeg` on `PATH`).
|
|
95
|
+
|
|
96
|
+
Same parameters as `frame_image`, plus `webm` (boolean); `background` defaults
|
|
97
|
+
to black for video. Returns JSON:
|
|
98
|
+
`{kind: "video", transparent, output, webm?, device, color, orientation, width, height}`.
|
|
99
|
+
|
|
100
|
+
### `list_devices`
|
|
101
|
+
|
|
102
|
+
List the 38-device bezel catalog (ids, colors, screen pixel sizes), or — with
|
|
103
|
+
`input_path` or `dimensions` (`"1206x2622"`) — find which devices fit a given
|
|
104
|
+
screenshot, recording, or pixel size. Filtered results return
|
|
105
|
+
`{width, height, matches, nearest}`; an empty `matches` array means no exact
|
|
106
|
+
fit and `nearest` lists the closest devices by aspect ratio.
|
|
107
|
+
|
|
108
|
+
## Error handling
|
|
109
|
+
|
|
110
|
+
CLI failures come back as MCP tool errors carrying the CLI's stderr — which
|
|
111
|
+
already includes concrete fixes (valid device ids, a device's color list,
|
|
112
|
+
nearest screen sizes) — plus the documented meaning of the exit code (2 unknown
|
|
113
|
+
or ambiguous device, 3 unknown color, 4 unreadable input, 5 composite/export
|
|
114
|
+
failed, 6 write failed, 7 ffmpeg missing for `--webm`).
|
|
115
|
+
|
|
116
|
+
## Environment variables
|
|
117
|
+
|
|
118
|
+
| Variable | Purpose |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `BEZELBUB_CLI_PATH` | Explicit path to the `bezelbub` binary (overrides `PATH` lookup) |
|
|
121
|
+
| `BEZELBUB_TIMEOUT_MS` | Per-call CLI timeout in milliseconds (default 600000; raise for long video exports) |
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
npm ci
|
|
127
|
+
npm run build # tsc → dist/
|
|
128
|
+
npm test # builds, then runs an end-to-end MCP-over-stdio test
|
|
129
|
+
# (requires the bezelbub CLI; generates its own test image)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT © DGR Labs
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution and execution of the `bezelbub` CLI binary.
|
|
3
|
+
*
|
|
4
|
+
* The MCP server is a thin adapter: every tool call shells out to
|
|
5
|
+
* `bezelbub` in `--json` mode and maps its exit codes / stderr to
|
|
6
|
+
* MCP tool errors.
|
|
7
|
+
*/
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
import { accessSync, constants } from "node:fs";
|
|
10
|
+
import { delimiter, join } from "node:path";
|
|
11
|
+
const BREW_HINT = "Install it with: brew install cwooddgr/tap/bezelbub — or point the " +
|
|
12
|
+
"BEZELBUB_CLI_PATH environment variable at a bezelbub binary.";
|
|
13
|
+
/** Directories checked even when they are missing from PATH (GUI apps like
|
|
14
|
+
* Claude Desktop launch MCP servers with a minimal PATH that often lacks
|
|
15
|
+
* the Homebrew bin directory). */
|
|
16
|
+
const FALLBACK_DIRS = ["/opt/homebrew/bin", "/usr/local/bin"];
|
|
17
|
+
/** Exit-code meanings documented in `bezelbub --help`. */
|
|
18
|
+
const EXIT_MEANINGS = {
|
|
19
|
+
1: "invalid flag value",
|
|
20
|
+
2: "unknown, ambiguous, or undetectable device",
|
|
21
|
+
3: "unknown color",
|
|
22
|
+
4: "input image or video unreadable",
|
|
23
|
+
5: "compositing or video export failed",
|
|
24
|
+
6: "output could not be written",
|
|
25
|
+
7: "WebM conversion failed (ffmpeg missing from PATH, or ffmpeg errored)",
|
|
26
|
+
64: "malformed arguments",
|
|
27
|
+
};
|
|
28
|
+
let cachedBinary;
|
|
29
|
+
function isExecutable(path) {
|
|
30
|
+
try {
|
|
31
|
+
accessSync(path, constants.X_OK);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Locate the bezelbub binary.
|
|
40
|
+
* Order: BEZELBUB_CLI_PATH env var → `bezelbub` on PATH (plus the standard
|
|
41
|
+
* Homebrew bin dirs) → error telling the user how to install it.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveBinary() {
|
|
44
|
+
if (process.platform !== "darwin") {
|
|
45
|
+
throw new Error("Bezelbub is macOS-only: the bezelbub CLI is a macOS-native binary " +
|
|
46
|
+
"(Core Graphics / AVFoundation). Run this MCP server on a Mac.");
|
|
47
|
+
}
|
|
48
|
+
if (cachedBinary)
|
|
49
|
+
return cachedBinary;
|
|
50
|
+
const envPath = process.env.BEZELBUB_CLI_PATH?.trim();
|
|
51
|
+
// "${..." guards against an MCPB host passing through an unresolved
|
|
52
|
+
// ${user_config...} placeholder when the user left the setting blank.
|
|
53
|
+
if (envPath && !envPath.startsWith("${")) {
|
|
54
|
+
if (!isExecutable(envPath)) {
|
|
55
|
+
throw new Error(`BEZELBUB_CLI_PATH is set to "${envPath}" but no executable exists there. ` +
|
|
56
|
+
BREW_HINT);
|
|
57
|
+
}
|
|
58
|
+
cachedBinary = envPath;
|
|
59
|
+
return envPath;
|
|
60
|
+
}
|
|
61
|
+
const pathDirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
62
|
+
for (const dir of [...pathDirs, ...FALLBACK_DIRS]) {
|
|
63
|
+
const candidate = join(dir, "bezelbub");
|
|
64
|
+
if (isExecutable(candidate)) {
|
|
65
|
+
cachedBinary = candidate;
|
|
66
|
+
return candidate;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw new Error("The bezelbub CLI was not found on PATH. " + BREW_HINT);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Run `bezelbub <args> --json`. Resolves with parsed JSON on success;
|
|
73
|
+
* throws an Error whose message carries the CLI's stderr (which already
|
|
74
|
+
* contains concrete suggestions — valid ids, nearest devices, etc.) plus
|
|
75
|
+
* the documented meaning of the exit code.
|
|
76
|
+
*/
|
|
77
|
+
export function runBezelbub(args, timeoutMs = Number(process.env.BEZELBUB_TIMEOUT_MS) || 10 * 60 * 1000) {
|
|
78
|
+
const binary = resolveBinary();
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
execFile(binary, [...args, "--json"], { timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
81
|
+
if (!error) {
|
|
82
|
+
let json = stdout;
|
|
83
|
+
try {
|
|
84
|
+
json = JSON.parse(stdout);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
/* leave raw */
|
|
88
|
+
}
|
|
89
|
+
resolve({ json, stdout });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const anyErr = error;
|
|
93
|
+
if (anyErr.code === "ENOENT") {
|
|
94
|
+
reject(new Error(`Could not execute bezelbub at "${binary}". ${BREW_HINT}`));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (anyErr.killed || anyErr.signal) {
|
|
98
|
+
reject(new Error(`bezelbub timed out after ${Math.round(timeoutMs / 1000)}s ` +
|
|
99
|
+
`(signal ${anyErr.signal}). Long video exports may need a larger ` +
|
|
100
|
+
`timeout: set the BEZELBUB_TIMEOUT_MS environment variable.`));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const code = typeof anyErr.code === "number" ? anyErr.code : undefined;
|
|
104
|
+
const meaning = code !== undefined && EXIT_MEANINGS[code]
|
|
105
|
+
? ` (${EXIT_MEANINGS[code]})`
|
|
106
|
+
: "";
|
|
107
|
+
const detail = (stderr || stdout || "no error output").trim();
|
|
108
|
+
reject(new Error(`bezelbub exited with code ${code}${meaning}:\n${detail}`));
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bezelbub-mcp — MCP server wrapping the `bezelbub` CLI.
|
|
4
|
+
*
|
|
5
|
+
* Composites Apple device bezels onto screenshots and screen recordings.
|
|
6
|
+
* macOS-only (the CLI is a macOS-native binary). Every tool shells out to
|
|
7
|
+
* `bezelbub --json`; see src/cli.ts for binary resolution and error mapping.
|
|
8
|
+
*/
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { resolve as resolvePath } from "node:path";
|
|
13
|
+
import { runBezelbub } from "./cli.js";
|
|
14
|
+
const SERVER_VERSION = "0.1.0";
|
|
15
|
+
const server = new McpServer({
|
|
16
|
+
name: "bezelbub",
|
|
17
|
+
version: SERVER_VERSION,
|
|
18
|
+
});
|
|
19
|
+
/** Shared framing options (image + video). */
|
|
20
|
+
const framingShape = {
|
|
21
|
+
input_path: z
|
|
22
|
+
.string()
|
|
23
|
+
.describe("Absolute path to the input file. Relative paths are resolved against " +
|
|
24
|
+
"the MCP server's working directory, so prefer absolute paths."),
|
|
25
|
+
device: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Device id, e.g. 'iphone17pro', 'ipadpro13m5', 'macbookpro14'. Omit to " +
|
|
29
|
+
"auto-detect from the input's pixel size; if several devices share " +
|
|
30
|
+
"that resolution the error lists the candidates. Use list_devices " +
|
|
31
|
+
"for valid ids."),
|
|
32
|
+
color: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Bezel color name, e.g. 'Silver', 'Cosmic Orange'. Defaults to the " +
|
|
36
|
+
"device's default color. Invalid colors error with the valid list."),
|
|
37
|
+
orientation: z
|
|
38
|
+
.enum(["portrait", "landscape", "auto"])
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Bezel orientation. Default 'auto' (inferred from the input)."),
|
|
41
|
+
output_size: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Scale the output, preserving the bezel's aspect ratio: a width " +
|
|
45
|
+
"('1920'), an exact size ('1920x988'), or a percentage of native " +
|
|
46
|
+
"size ('50%'). Default: the bezel's native size."),
|
|
47
|
+
output_path: z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("Absolute path for the output file. Defaults to '<input>-framed.png' " +
|
|
51
|
+
"(image) or '<input>-framed.mp4' / '.mov' (video) beside the input."),
|
|
52
|
+
};
|
|
53
|
+
function framingFlags(args) {
|
|
54
|
+
const flags = ["frame", "--input", resolvePath(args.input_path)];
|
|
55
|
+
if (args.device)
|
|
56
|
+
flags.push("--device", args.device);
|
|
57
|
+
if (args.color)
|
|
58
|
+
flags.push("--color", args.color);
|
|
59
|
+
if (args.orientation)
|
|
60
|
+
flags.push("--orientation", args.orientation);
|
|
61
|
+
if (args.output_size)
|
|
62
|
+
flags.push("--output-size", args.output_size);
|
|
63
|
+
if (args.output_path)
|
|
64
|
+
flags.push("--output", resolvePath(args.output_path));
|
|
65
|
+
return flags;
|
|
66
|
+
}
|
|
67
|
+
function ok(json, stdout) {
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: typeof json === "string" ? stdout : JSON.stringify(json, null, 2),
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function fail(error) {
|
|
78
|
+
return {
|
|
79
|
+
content: [
|
|
80
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) },
|
|
81
|
+
],
|
|
82
|
+
isError: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
server.registerTool("frame_image", {
|
|
86
|
+
title: "Frame a screenshot in a device bezel",
|
|
87
|
+
description: "Frame a screenshot in an Apple device bezel — i.e. make a device " +
|
|
88
|
+
"mockup: put a screenshot inside a realistic iPhone, iPad, Mac, iMac, " +
|
|
89
|
+
"or Apple TV frame. Use for requests like 'frame this screenshot in an " +
|
|
90
|
+
"iPhone bezel', 'device mockup', 'make this look like it's on an iPad', " +
|
|
91
|
+
"or App Store / marketing imagery. Input: PNG, JPEG, or HEIC file on " +
|
|
92
|
+
"disk. Output: a framed PNG written next to the input (or at " +
|
|
93
|
+
"output_path); the background around the bezel is transparent unless a " +
|
|
94
|
+
"hex color is given. The device is auto-detected from the screenshot's " +
|
|
95
|
+
"pixel size when 'device' is omitted. Returns JSON with the output " +
|
|
96
|
+
"path, device, color, orientation, and final pixel size. macOS only.",
|
|
97
|
+
inputSchema: {
|
|
98
|
+
...framingShape,
|
|
99
|
+
background: z
|
|
100
|
+
.string()
|
|
101
|
+
.optional()
|
|
102
|
+
.describe("Background fill behind/around the bezel: a hex color like " +
|
|
103
|
+
"'#FFFFFF' or '#RRGGBBAA', or 'transparent'. Default: transparent."),
|
|
104
|
+
},
|
|
105
|
+
annotations: {
|
|
106
|
+
readOnlyHint: false,
|
|
107
|
+
destructiveHint: false,
|
|
108
|
+
idempotentHint: true,
|
|
109
|
+
openWorldHint: false,
|
|
110
|
+
},
|
|
111
|
+
}, async (args) => {
|
|
112
|
+
try {
|
|
113
|
+
const flags = framingFlags(args);
|
|
114
|
+
if (args.background)
|
|
115
|
+
flags.push("--background", args.background);
|
|
116
|
+
const { json, stdout } = await runBezelbub(flags);
|
|
117
|
+
return ok(json, stdout);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
return fail(error);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
server.registerTool("frame_video", {
|
|
124
|
+
title: "Frame a screen recording in a device bezel",
|
|
125
|
+
description: "Frame a screen recording (video) in an Apple device bezel — a video " +
|
|
126
|
+
"device mockup: put an iPhone/iPad/Mac screen recording inside a " +
|
|
127
|
+
"realistic device frame, preserving the audio track. Use for requests " +
|
|
128
|
+
"like 'frame this screen recording in an iPhone bezel', 'app demo " +
|
|
129
|
+
"video in a device frame', or 'transparent video with alpha for my " +
|
|
130
|
+
"website'. Input: .mov, .mp4, or .m4v file on disk. Output: a framed " +
|
|
131
|
+
"MP4 (background defaults to black), or — with background " +
|
|
132
|
+
"'transparent' — an HEVC-with-alpha QuickTime .mov whose surroundings " +
|
|
133
|
+
"are truly transparent (plays in Safari and Apple frameworks only; set " +
|
|
134
|
+
"webm=true to also write a VP9/WebM copy for Chrome/Firefox, which " +
|
|
135
|
+
"requires ffmpeg on PATH). Device is auto-detected from the video's " +
|
|
136
|
+
"pixel size when 'device' is omitted. Returns JSON with the output " +
|
|
137
|
+
"path(s) and final pixel size. Long videos can take minutes. macOS only.",
|
|
138
|
+
inputSchema: {
|
|
139
|
+
...framingShape,
|
|
140
|
+
background: z
|
|
141
|
+
.string()
|
|
142
|
+
.optional()
|
|
143
|
+
.describe("Background fill: a hex color like '#1D1D1F', or 'transparent'. " +
|
|
144
|
+
"Default: black. 'transparent' switches the export to " +
|
|
145
|
+
"HEVC-with-alpha in a QuickTime .mov (Safari/Apple playback only)."),
|
|
146
|
+
webm: z
|
|
147
|
+
.boolean()
|
|
148
|
+
.optional()
|
|
149
|
+
.describe("Also write a VP9/WebM copy of a transparent export for " +
|
|
150
|
+
"Chrome/Firefox playback. Requires background='transparent' and " +
|
|
151
|
+
"ffmpeg on PATH."),
|
|
152
|
+
},
|
|
153
|
+
annotations: {
|
|
154
|
+
readOnlyHint: false,
|
|
155
|
+
destructiveHint: false,
|
|
156
|
+
idempotentHint: true,
|
|
157
|
+
openWorldHint: false,
|
|
158
|
+
},
|
|
159
|
+
}, async (args) => {
|
|
160
|
+
try {
|
|
161
|
+
const flags = framingFlags(args);
|
|
162
|
+
if (args.background)
|
|
163
|
+
flags.push("--background", args.background);
|
|
164
|
+
if (args.webm)
|
|
165
|
+
flags.push("--webm");
|
|
166
|
+
const { json, stdout } = await runBezelbub(flags);
|
|
167
|
+
return ok(json, stdout);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
return fail(error);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
server.registerTool("list_devices", {
|
|
174
|
+
title: "List device bezels / match a screenshot to devices",
|
|
175
|
+
description: "List the Apple device bezels available for framing (iPhones, iPads, " +
|
|
176
|
+
"MacBooks, iMac, Apple TV) with their ids, color options, and screen " +
|
|
177
|
+
"pixel sizes — or find which devices fit a given screenshot, screen " +
|
|
178
|
+
"recording, or WxH pixel size ('which iPhone matches a 1206x2622 " +
|
|
179
|
+
"screenshot?'). With input_path or dimensions, returns {width, height, " +
|
|
180
|
+
"matches, nearest}: 'matches' are exact screen-size fits (empty means " +
|
|
181
|
+
"no device fits; 'nearest' then lists the closest by aspect ratio). " +
|
|
182
|
+
"Without a filter, returns the full device catalog. Use this before " +
|
|
183
|
+
"frame_image/frame_video when the device or color is ambiguous. " +
|
|
184
|
+
"macOS only.",
|
|
185
|
+
inputSchema: {
|
|
186
|
+
input_path: z
|
|
187
|
+
.string()
|
|
188
|
+
.optional()
|
|
189
|
+
.describe("Absolute path to a screenshot or video: filters the list to " +
|
|
190
|
+
"devices whose screen matches its pixel size."),
|
|
191
|
+
dimensions: z
|
|
192
|
+
.string()
|
|
193
|
+
.optional()
|
|
194
|
+
.describe("Filter by a pixel size like '1206x2622' (width x height) without " +
|
|
195
|
+
"needing a file on disk. Ignored if input_path is given."),
|
|
196
|
+
},
|
|
197
|
+
annotations: {
|
|
198
|
+
readOnlyHint: true,
|
|
199
|
+
openWorldHint: false,
|
|
200
|
+
},
|
|
201
|
+
}, async (args) => {
|
|
202
|
+
try {
|
|
203
|
+
const flags = ["devices"];
|
|
204
|
+
if (args.input_path)
|
|
205
|
+
flags.push("--input", resolvePath(args.input_path));
|
|
206
|
+
else if (args.dimensions)
|
|
207
|
+
flags.push("--dimensions", args.dimensions);
|
|
208
|
+
const { json, stdout } = await runBezelbub(flags);
|
|
209
|
+
return ok(json, stdout);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
return fail(error);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
async function main() {
|
|
216
|
+
const transport = new StdioServerTransport();
|
|
217
|
+
await server.connect(transport);
|
|
218
|
+
console.error(`bezelbub-mcp ${SERVER_VERSION} ready (stdio)`);
|
|
219
|
+
}
|
|
220
|
+
main().catch((error) => {
|
|
221
|
+
console.error("bezelbub-mcp fatal:", error);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
});
|
package/manifest.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": "0.3",
|
|
3
|
+
"name": "bezelbub-mcp",
|
|
4
|
+
"display_name": "Bezelbub",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"description": "Frame screenshots and screen recordings in Apple device bezels (iPhone, iPad, Mac, iMac, Apple TV mockups).",
|
|
7
|
+
"long_description": "Bezelbub composites realistic Apple device bezels onto screenshots and screen recordings — device mockups for App Store pages, marketing sites, and demo videos.\n\nTools: frame a screenshot as a framed PNG, frame a screen recording as a framed MP4 (or a transparent HEVC-with-alpha .mov, with an optional VP9/WebM copy for Chrome/Firefox), and list/match the device catalog (38 devices: iPhone 14–17 families, iPads, MacBooks, iMac, Apple TV).\n\nRequires the bezelbub CLI on this Mac: `brew install cwooddgr/tap/bezelbub`. macOS only — the engine is a macOS-native binary.",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "DGR Labs",
|
|
10
|
+
"url": "https://dgrlabs.co"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/cwooddgr/bezelbub.git"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://dgrlabs.co",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"bezel",
|
|
19
|
+
"device mockup",
|
|
20
|
+
"device frame",
|
|
21
|
+
"screenshot",
|
|
22
|
+
"screen recording",
|
|
23
|
+
"iphone",
|
|
24
|
+
"ipad",
|
|
25
|
+
"mac",
|
|
26
|
+
"app store",
|
|
27
|
+
"transparent video",
|
|
28
|
+
"hevc alpha"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"server": {
|
|
32
|
+
"type": "node",
|
|
33
|
+
"entry_point": "dist/index.js",
|
|
34
|
+
"mcp_config": {
|
|
35
|
+
"command": "node",
|
|
36
|
+
"args": ["${__dirname}/dist/index.js"],
|
|
37
|
+
"env": {
|
|
38
|
+
"BEZELBUB_CLI_PATH": "${user_config.bezelbub_cli_path}"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"user_config": {
|
|
43
|
+
"bezelbub_cli_path": {
|
|
44
|
+
"type": "file",
|
|
45
|
+
"title": "bezelbub CLI path",
|
|
46
|
+
"description": "Path to the bezelbub binary. Leave blank if it is installed via Homebrew (brew install cwooddgr/tap/bezelbub) — the server finds it in /opt/homebrew/bin or /usr/local/bin automatically.",
|
|
47
|
+
"required": false
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"tools": [
|
|
51
|
+
{
|
|
52
|
+
"name": "frame_image",
|
|
53
|
+
"description": "Frame a screenshot in an Apple device bezel (device mockup); writes a framed PNG."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "frame_video",
|
|
57
|
+
"description": "Frame a screen recording in a device bezel; framed MP4, or transparent HEVC-alpha .mov with optional WebM copy."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "list_devices",
|
|
61
|
+
"description": "List available device bezels, or match a screenshot/recording/pixel size to devices."
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"compatibility": {
|
|
65
|
+
"platforms": ["darwin"],
|
|
66
|
+
"runtimes": {
|
|
67
|
+
"node": ">=18.0.0"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dgr_labs/bezelbub-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"mcpName": "io.github.cwooddgr/bezelbub-mcp",
|
|
5
|
+
"description": "MCP server for Bezelbub: frame screenshots and screen recordings in Apple device bezels (iPhone, iPad, Mac, iMac, Apple TV device mockups), including transparent HEVC-alpha video export. macOS only.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"bezelbub-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"manifest.json",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"prepublishOnly": "npm run build",
|
|
19
|
+
"test": "npm run build && node test/e2e.mjs"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"os": [
|
|
25
|
+
"darwin"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"mcp",
|
|
29
|
+
"model-context-protocol",
|
|
30
|
+
"mcp-server",
|
|
31
|
+
"bezel",
|
|
32
|
+
"device-mockup",
|
|
33
|
+
"device-frame",
|
|
34
|
+
"screenshot",
|
|
35
|
+
"screen-recording",
|
|
36
|
+
"iphone",
|
|
37
|
+
"ipad",
|
|
38
|
+
"mac",
|
|
39
|
+
"app-store",
|
|
40
|
+
"hevc-alpha",
|
|
41
|
+
"transparent-video",
|
|
42
|
+
"macos"
|
|
43
|
+
],
|
|
44
|
+
"author": {
|
|
45
|
+
"name": "DGR Labs",
|
|
46
|
+
"url": "https://dgrlabs.co"
|
|
47
|
+
},
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/cwooddgr/bezelbub.git",
|
|
52
|
+
"directory": "bezelbub-mcp"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
56
|
+
"zod": "^4.4.3"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^26.1.1",
|
|
60
|
+
"typescript": "^7.0.2"
|
|
61
|
+
}
|
|
62
|
+
}
|