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 +93 -0
- package/dist/index.js +98 -0
- package/package.json +30 -0
- package/server.json +26 -0
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
|
+
}
|