imgcli-mcp 0.2.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 ADDED
@@ -0,0 +1,93 @@
1
+ # imgcli-mcp
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server that exposes
4
+ [imgcli](https://github.com/swperb/imgcli) as native tools for AI agents, so a
5
+ model can convert and process images by calling tools instead of shelling out.
6
+
7
+ ## Tools
8
+
9
+ | Tool | Purpose |
10
+ | --- | --- |
11
+ | `convert_image` | Convert / resize / crop / rotate / filter / composite an image. Args: `input`, `output`, optional `filters` (ffmpeg-style chain), `quality`, `overwrite`. Returns imgcli's JSON result. |
12
+ | `probe_image` | Return an image's width/height/channels without converting. |
13
+ | `list_filters` | List the full filter catalogue. |
14
+
15
+ All arguments are passed to imgcli as an argv array (no shell), so there is no
16
+ shell-injection surface.
17
+
18
+ ## Prerequisites
19
+
20
+ The `imgcli` binary must be installed:
21
+
22
+ ```sh
23
+ brew install swperb/tap/imgcli # or build from source: make && sudo make install
24
+ ```
25
+
26
+ If it isn't on `PATH`, set `IMGCLI_BIN` to its absolute path.
27
+
28
+ ## Run
29
+
30
+ ```sh
31
+ npm install # builds dist/ via the prepare script
32
+ npm start # stdio MCP server
33
+ # or, once published to npm:
34
+ npx imgcli-mcp
35
+ ```
36
+
37
+ ## Configure in a client
38
+
39
+ Claude Desktop / Cursor / any MCP client (`mcpServers` config):
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "imgcli": {
45
+ "command": "npx",
46
+ "args": ["-y", "imgcli-mcp"],
47
+ "env": { "IMGCLI_BIN": "imgcli" }
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ Or point `command` at `node` and `args` at the built `dist/index.js` for a local checkout.
54
+
55
+ ## Example calls
56
+
57
+ ```jsonc
58
+ // convert_image
59
+ { "input": "photo.jpg", "output": "thumb.png", "filters": "scale=256:-1,grayscale" }
60
+ // -> {"ok":true,"output":"thumb.png","width":256,"height":171,"format":"png","bytes":34122}
61
+
62
+ // probe_image
63
+ { "input": "photo.jpg" }
64
+ // -> {"ok":true,"inputs":[{"path":"photo.jpg","width":4000,"height":3000,"channels":4}]}
65
+ ```
66
+
67
+ ## Publishing
68
+
69
+ Two stages — **npm first**, then the **MCP registry** (the registry verifies the
70
+ npm package's `mcpName` field against the server `name`).
71
+
72
+ ```sh
73
+ # 1. Publish to npm (needs `npm login`). Package is public via publishConfig.
74
+ cd mcp
75
+ npm publish
76
+
77
+ # 2. Publish metadata to the MCP registry
78
+ brew install mcp-publisher # (or download from the registry's GitHub releases)
79
+ mcp-publisher login github # device-flow auth as the io.github.swperb owner
80
+ mcp-publisher validate # optional: check server.json
81
+ mcp-publisher publish # publishes server.json
82
+
83
+ # verify
84
+ curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.swperb/imgcli"
85
+ ```
86
+
87
+ Ownership is proven by `mcpName` in `package.json` matching `name` in
88
+ `server.json` (`io.github.swperb/imgcli`). On every release keep three versions
89
+ in sync: `package.json` `version`, `server.json` `version`, and the
90
+ `server.json` `packages[].version`.
91
+
92
+ > The MCP registry is in preview; data resets can occur, so be ready to
93
+ > re-publish. There is no self-service unpublish — publish a new version to update.
package/dist/index.js ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * imgcli-mcp — an MCP server that exposes the `imgcli` command-line image tool
4
+ * as native tools for AI agents (Claude Desktop, Cursor, etc.).
5
+ *
6
+ * Tools: convert_image, probe_image, list_filters.
7
+ *
8
+ * The `imgcli` binary must be installed and on PATH (brew install swperb/tap/imgcli)
9
+ * or pointed to via the IMGCLI_BIN environment variable. Arguments are passed as
10
+ * an argv array (execFile, no shell) so there is no shell-injection surface.
11
+ */
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import { z } from "zod";
15
+ import { execFile } from "node:child_process";
16
+ const BIN = process.env.IMGCLI_BIN || "imgcli";
17
+ function run(args) {
18
+ return new Promise((resolve) => {
19
+ execFile(BIN, args, { timeout: 60_000, maxBuffer: 8 * 1024 * 1024 }, (err, stdout, stderr) => {
20
+ const code = err && typeof err.code === "number"
21
+ ? (err.code)
22
+ : err ? 1 : 0;
23
+ resolve({ code, stdout: stdout ?? "", stderr: stderr ?? "" });
24
+ });
25
+ });
26
+ }
27
+ /** Parse imgcli's JSON line from stdout (success) or stderr (error). */
28
+ function parseJsonLine(text) {
29
+ const line = text.trim().split("\n").filter(Boolean).pop();
30
+ if (!line)
31
+ return null;
32
+ try {
33
+ return JSON.parse(line);
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ const server = new McpServer({ name: "imgcli", version: "0.2.0" });
40
+ server.registerTool("convert_image", {
41
+ title: "Convert / process an image",
42
+ description: "Convert, resize, crop, rotate, filter, or composite an image with imgcli. " +
43
+ "Output format is chosen from the output file extension (png/jpg/jpeg/bmp/tga/ppm). " +
44
+ "Use `filters` for an ffmpeg-style comma-separated chain, e.g. " +
45
+ '"scale=800:-1,grayscale,gblur=2". Call list_filters for the full catalogue.',
46
+ inputSchema: {
47
+ input: z.string().describe("Path to the input image file (or a generator like 'testsrc=640x480')."),
48
+ output: z.string().describe("Path to write; the extension picks the format (png/jpg/bmp/tga/ppm)."),
49
+ filters: z.string().optional().describe('Filtergraph, e.g. "scale=512:-1,grayscale". Omit for a plain format conversion.'),
50
+ quality: z.number().int().min(1).max(100).optional().describe("JPEG quality 1-100 (default 90; ignored for non-JPEG)."),
51
+ overwrite: z.boolean().optional().describe("Overwrite the output if it exists (default true)."),
52
+ },
53
+ }, async ({ input, output, filters, quality, overwrite }) => {
54
+ const args = ["--json", "-i", input];
55
+ if (filters)
56
+ args.push("-vf", filters);
57
+ if (quality != null)
58
+ args.push("-q", String(quality));
59
+ args.push(overwrite === false ? "-n" : "-y", output);
60
+ const { code, stdout, stderr } = await run(args);
61
+ const result = parseJsonLine(code === 0 ? stdout : stderr) ?? {
62
+ ok: false,
63
+ error: (stderr || stdout || `imgcli exited with code ${code}`).trim(),
64
+ };
65
+ return {
66
+ content: [{ type: "text", text: JSON.stringify(result) }],
67
+ isError: code !== 0,
68
+ };
69
+ });
70
+ server.registerTool("probe_image", {
71
+ title: "Probe image dimensions",
72
+ description: "Return the width, height, and channel count of an image without converting it.",
73
+ inputSchema: { input: z.string().describe("Path to the input image file.") },
74
+ }, async ({ input }) => {
75
+ const { code, stdout, stderr } = await run(["--json", "-info", "-i", input]);
76
+ const result = parseJsonLine(code === 0 ? stdout : stderr) ?? {
77
+ ok: false,
78
+ error: (stderr || stdout || `imgcli exited with code ${code}`).trim(),
79
+ };
80
+ return { content: [{ type: "text", text: JSON.stringify(result) }], isError: code !== 0 };
81
+ });
82
+ server.registerTool("list_filters", {
83
+ title: "List available filters",
84
+ description: "List the full imgcli filter catalogue (geometry, colour, convolution, compositing).",
85
+ inputSchema: {},
86
+ }, async () => {
87
+ const { stdout } = await run(["-filters"]);
88
+ return { content: [{ type: "text", text: stdout.trim() || "no output" }] };
89
+ });
90
+ async function main() {
91
+ await server.connect(new StdioServerTransport());
92
+ // stderr is safe for logs; stdout is reserved for the MCP protocol.
93
+ process.stderr.write(`imgcli-mcp ready (binary: ${BIN})\n`);
94
+ }
95
+ main().catch((e) => {
96
+ process.stderr.write(`imgcli-mcp fatal: ${e instanceof Error ? e.message : String(e)}\n`);
97
+ process.exit(1);
98
+ });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "imgcli-mcp",
3
+ "version": "0.2.0",
4
+ "mcpName": "io.github.swperb/imgcli",
5
+ "description": "MCP server exposing imgcli — convert, resize, crop, filter & composite images as native agent tools",
6
+ "keywords": ["mcp", "model-context-protocol", "image", "image-conversion", "imgcli", "resize", "convert", "ffmpeg", "imagemagick", "agent", "tool"],
7
+ "homepage": "https://github.com/swperb/imgcli/tree/main/mcp",
8
+ "repository": { "type": "git", "url": "git+https://github.com/swperb/imgcli.git", "directory": "mcp" },
9
+ "license": "MIT",
10
+ "author": "swperb",
11
+ "bugs": { "url": "https://github.com/swperb/imgcli/issues" },
12
+ "publishConfig": { "access": "public" },
13
+ "type": "module",
14
+ "bin": { "imgcli-mcp": "dist/index.js" },
15
+ "files": ["dist", "README.md", "server.json"],
16
+ "engines": { "node": ">=18" },
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "prepare": "npm run build",
20
+ "start": "node dist/index.js"
21
+ },
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.17.0",
24
+ "zod": "^3.23.8"
25
+ },
26
+ "devDependencies": {
27
+ "typescript": "^5.6.0",
28
+ "@types/node": "^20.14.0"
29
+ }
30
+ }
package/server.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.swperb/imgcli",
4
+ "description": "Convert, resize, crop, filter & composite images via the imgcli CLI (lightweight, no deps)",
5
+ "version": "0.2.0",
6
+ "repository": {
7
+ "url": "https://github.com/swperb/imgcli",
8
+ "source": "github"
9
+ },
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "registryBaseUrl": "https://registry.npmjs.org",
14
+ "identifier": "imgcli-mcp",
15
+ "version": "0.2.0",
16
+ "transport": { "type": "stdio" },
17
+ "environmentVariables": [
18
+ {
19
+ "name": "IMGCLI_BIN",
20
+ "description": "Path to the imgcli binary if not on PATH (e.g. after `brew install swperb/tap/imgcli`).",
21
+ "isRequired": false
22
+ }
23
+ ]
24
+ }
25
+ ]
26
+ }