@apresai/gimage-mcp 1.2.135 → 1.2.139

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,56 @@
1
+ # @apresai/gimage-mcp
2
+
3
+ MCP (Model Context Protocol) server for AI-powered image generation and processing,
4
+ backed by the [`gimage`](https://github.com/apresai/gimage) CLI. Exposes image
5
+ generation (Gemini, Vertex AI, AWS Bedrock, xAI Grok) plus resize/scale/crop/compress/
6
+ convert and batch operations as MCP tools for Claude Desktop and other MCP clients.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install -g @apresai/gimage-mcp
12
+ ```
13
+
14
+ This package is a thin Node wrapper. On install it downloads the platform-native
15
+ `gimage` binary from the matching GitHub release into the package. If your package
16
+ manager blocks postinstall scripts, the binary is fetched automatically on first run,
17
+ or it falls back to a `gimage` already on your `PATH` (e.g. installed via Homebrew:
18
+ `brew install apresai/tap/gimage`).
19
+
20
+ ## Use with Claude Desktop
21
+
22
+ Add to your MCP configuration
23
+ (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
24
+
25
+ ```json
26
+ {
27
+ "mcpServers": {
28
+ "gimage": {
29
+ "command": "npx",
30
+ "args": ["-y", "@apresai/gimage-mcp"]
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ Running `gimage-mcp` with no arguments starts the MCP server over stdio. Any arguments
37
+ are forwarded to the underlying `gimage` CLI, so `gimage-mcp --version` prints the
38
+ version and `gimage-mcp serve` is equivalent to the default.
39
+
40
+ ## Tools
41
+
42
+ `generate_image`, `resize_image`, `scale_image`, `crop_image`, `compress_image`,
43
+ `convert_image`, `batch_resize`, `batch_compress`, `batch_convert`, `list_models`.
44
+
45
+ ## Configure API keys
46
+
47
+ ```bash
48
+ gimage auth setup gemini
49
+ ```
50
+
51
+ See the [main project README](https://github.com/apresai/gimage#readme) for full
52
+ documentation, supported models, and pricing.
53
+
54
+ ## License
55
+
56
+ MIT
package/gimage-mcp.js ADDED
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn, spawnSync } = require('child_process');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ // The platform gimage binary lives beside this wrapper at <package>/bin/gimage.
8
+ function packageBinaryPath() {
9
+ const ext = process.platform === 'win32' ? '.exe' : '';
10
+ return path.join(__dirname, 'bin', `gimage${ext}`);
11
+ }
12
+
13
+ // Resolve a system-installed gimage (e.g. via Homebrew) on PATH to its full
14
+ // path, or null if absent. Returning the resolved path (rather than the bare
15
+ // name) lets spawn launch it directly — important on Windows, where
16
+ // spawn('gimage') without a shell does not append .exe / search PATHEXT.
17
+ function systemGimagePath() {
18
+ const probe = process.platform === 'win32' ? 'where' : 'which';
19
+ const result = spawnSync(probe, ['gimage'], { encoding: 'utf8' });
20
+ if (result.status !== 0 || !result.stdout) {
21
+ return null;
22
+ }
23
+ // `where` may list several matches; take the first line.
24
+ const first = result.stdout.split(/\r?\n/)[0].trim();
25
+ return first || null;
26
+ }
27
+
28
+ // Resolve the gimage binary, in order of preference:
29
+ // 1. the version-pinned binary bundled into this package (downloaded by the
30
+ // postinstall hook),
31
+ // 2. a system gimage on PATH (Homebrew, manual install),
32
+ // 3. a lazy download into the package bin/ — this covers the case where npm's
33
+ // allow-scripts gate blocked postinstall so the binary was never fetched.
34
+ async function resolveBinary() {
35
+ const pkgBinary = packageBinaryPath();
36
+ if (fs.existsSync(pkgBinary)) {
37
+ return pkgBinary;
38
+ }
39
+ const systemBinary = systemGimagePath();
40
+ if (systemBinary) {
41
+ return systemBinary;
42
+ }
43
+ // install.js logs progress to stderr, so this is safe to run right before
44
+ // launching the MCP server (whose stdout is the JSON-RPC channel).
45
+ const { ensureBinary } = require('./scripts/install.js');
46
+ return ensureBinary();
47
+ }
48
+
49
+ async function main() {
50
+ let binaryPath;
51
+ try {
52
+ binaryPath = await resolveBinary();
53
+ } catch (error) {
54
+ console.error('Failed to locate or download the gimage binary:', error.message);
55
+ console.error('\nInstall gimage manually:');
56
+ console.error(' npm install -g @apresai/gimage-mcp');
57
+ console.error(' or');
58
+ console.error(' brew install apresai/tap/gimage');
59
+ console.error('\nSee: https://github.com/apresai/gimage');
60
+ process.exit(1);
61
+ }
62
+
63
+ // Forward any args straight through to gimage. With no args, default to the MCP
64
+ // server so `npx -y @apresai/gimage-mcp` launches it for Claude Desktop, while
65
+ // `gimage-mcp --version`, `gimage-mcp serve`, etc. pass through unchanged.
66
+ const forwarded = process.argv.slice(2);
67
+ const args = forwarded.length > 0 ? forwarded : ['serve'];
68
+
69
+ const child = spawn(binaryPath, args, {
70
+ stdio: 'inherit',
71
+ env: process.env
72
+ });
73
+
74
+ child.on('error', (error) => {
75
+ console.error('Failed to start gimage:', error.message);
76
+ console.error('\nPlease ensure gimage is installed:');
77
+ console.error(' npm install -g @apresai/gimage-mcp');
78
+ console.error(' or');
79
+ console.error(' brew install apresai/tap/gimage');
80
+ process.exit(1);
81
+ });
82
+
83
+ child.on('exit', (code) => {
84
+ process.exit(code || 0);
85
+ });
86
+
87
+ // Forward termination signals for graceful shutdown.
88
+ process.on('SIGINT', () => child.kill('SIGINT'));
89
+ process.on('SIGTERM', () => child.kill('SIGTERM'));
90
+ }
91
+
92
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apresai/gimage-mcp",
3
- "version": "1.2.135",
3
+ "version": "1.2.139",
4
4
  "description": "MCP server for AI-powered image generation and processing with gimage",
5
5
  "keywords": [
6
6
  "mcp",
@@ -26,10 +26,11 @@
26
26
  "url": "https://github.com/apresai/gimage/issues"
27
27
  },
28
28
  "bin": {
29
- "gimage-mcp": "./bin/gimage-mcp.js"
29
+ "gimage-mcp": "./gimage-mcp.js"
30
30
  },
31
31
  "scripts": {
32
- "postinstall": "node scripts/install.js"
32
+ "postinstall": "node scripts/install.js",
33
+ "test": "node --test"
33
34
  },
34
35
  "engines": {
35
36
  "node": ">=22.0.0"
@@ -44,7 +45,7 @@
44
45
  "arm64"
45
46
  ],
46
47
  "files": [
47
- "bin/",
48
+ "gimage-mcp.js",
48
49
  "scripts/",
49
50
  "README.md"
50
51
  ],
@@ -6,7 +6,10 @@ const path = require('path');
6
6
  const tar = require('tar');
7
7
 
8
8
  const GITHUB_REPO = 'apresai/gimage';
9
- const VERSION = process.env.npm_package_version || '0.1.0';
9
+ // npm sets npm_package_version during the install lifecycle (postinstall). When
10
+ // this module is require()'d at runtime by the bin wrapper that env var is unset,
11
+ // so fall back to the package's own version.
12
+ const VERSION = process.env.npm_package_version || require('../package.json').version;
10
13
 
11
14
  function getPlatformInfo() {
12
15
  const platform = process.platform;
@@ -27,7 +30,11 @@ function getPlatformInfo() {
27
30
  const mappedArch = archMap[arch];
28
31
 
29
32
  if (!mappedPlatform || !mappedArch) {
30
- throw new Error(`Unsupported platform: ${platform}-${arch}`);
33
+ throw new Error(
34
+ `Unsupported platform: ${platform}-${arch}. ` +
35
+ `Install gimage manually from https://github.com/${GITHUB_REPO}/releases ` +
36
+ `or via Homebrew: brew install apresai/tap/gimage`
37
+ );
31
38
  }
32
39
 
33
40
  return {
@@ -37,6 +44,13 @@ function getPlatformInfo() {
37
44
  };
38
45
  }
39
46
 
47
+ // binaryPath returns where the platform gimage binary lives (or will live) inside
48
+ // the package: <package>/bin/gimage[.exe].
49
+ function binaryPath() {
50
+ const { ext } = getPlatformInfo();
51
+ return path.join(__dirname, '..', 'bin', `gimage${ext}`);
52
+ }
53
+
40
54
  async function downloadBinary() {
41
55
  const { platform, arch, ext } = getPlatformInfo();
42
56
  const binaryName = `gimage${ext}`;
@@ -51,100 +65,74 @@ async function downloadBinary() {
51
65
  const url = `https://github.com/${GITHUB_REPO}/releases/download/v${VERSION}/${tarballName}`;
52
66
 
53
67
  const binDir = path.join(__dirname, '..', 'bin');
54
- const binaryPath = path.join(binDir, binaryName);
68
+ const binaryDest = path.join(binDir, binaryName);
55
69
 
56
70
  // Create bin directory if it doesn't exist
57
71
  if (!fs.existsSync(binDir)) {
58
72
  fs.mkdirSync(binDir, { recursive: true });
59
73
  }
60
74
 
61
- console.log(`Downloading gimage binary for ${platform}-${arch}...`);
62
- console.log(`URL: ${url}`);
75
+ // Progress goes to stderr so it never corrupts the MCP server's stdout
76
+ // JSON-RPC stream when this runs as a lazy download just before `gimage serve`.
77
+ console.error(`Downloading gimage binary for ${platform}-${arch}...`);
78
+ console.error(`URL: ${url}`);
79
+
80
+ function extractFrom(response, resolve, reject) {
81
+ const tarPath = path.join(binDir, tarballName);
82
+ const file = fs.createWriteStream(tarPath);
83
+ response.pipe(file);
84
+ file.on('finish', async () => {
85
+ file.close();
86
+ try {
87
+ await tar.x({ file: tarPath, cwd: binDir });
88
+ fs.unlinkSync(tarPath);
89
+ if (platform !== 'windows') {
90
+ fs.chmodSync(binaryDest, 0o755);
91
+ }
92
+ console.error('✓ gimage binary installed successfully');
93
+ resolve();
94
+ } catch (err) {
95
+ reject(err);
96
+ }
97
+ });
98
+ file.on('error', reject);
99
+ }
63
100
 
64
101
  return new Promise((resolve, reject) => {
65
102
  https.get(url, (response) => {
66
103
  if (response.statusCode === 302 || response.statusCode === 301) {
67
- // Follow redirect
104
+ // Follow redirect (GitHub release assets redirect to a CDN)
68
105
  https.get(response.headers.location, (redirectResponse) => {
69
106
  if (redirectResponse.statusCode !== 200) {
70
- reject(new Error(`Download failed with status ${redirectResponse.statusCode}`));
107
+ reject(new Error(`Download failed with status ${redirectResponse.statusCode} for ${url}`));
71
108
  return;
72
109
  }
73
-
74
- const tarPath = path.join(binDir, tarballName);
75
- const file = fs.createWriteStream(tarPath);
76
-
77
- redirectResponse.pipe(file);
78
-
79
- file.on('finish', async () => {
80
- file.close();
81
-
82
- try {
83
- // Extract tarball
84
- await tar.x({
85
- file: tarPath,
86
- cwd: binDir
87
- });
88
-
89
- // Remove tarball
90
- fs.unlinkSync(tarPath);
91
-
92
- // Make binary executable (Unix-like systems)
93
- if (platform !== 'windows') {
94
- fs.chmodSync(binaryPath, 0o755);
95
- }
96
-
97
- console.log('✓ gimage binary installed successfully');
98
- resolve();
99
- } catch (err) {
100
- reject(err);
101
- }
102
- });
103
-
104
- file.on('error', reject);
110
+ extractFrom(redirectResponse, resolve, reject);
105
111
  }).on('error', reject);
106
112
  } else if (response.statusCode === 200) {
107
- const tarPath = path.join(binDir, tarballName);
108
- const file = fs.createWriteStream(tarPath);
109
-
110
- response.pipe(file);
111
-
112
- file.on('finish', async () => {
113
- file.close();
114
-
115
- try {
116
- // Extract tarball
117
- await tar.x({
118
- file: tarPath,
119
- cwd: binDir
120
- });
121
-
122
- // Remove tarball
123
- fs.unlinkSync(tarPath);
124
-
125
- // Make binary executable (Unix-like systems)
126
- if (platform !== 'windows') {
127
- fs.chmodSync(binaryPath, 0o755);
128
- }
129
-
130
- console.log('✓ gimage binary installed successfully');
131
- resolve();
132
- } catch (err) {
133
- reject(err);
134
- }
135
- });
136
-
137
- file.on('error', reject);
113
+ extractFrom(response, resolve, reject);
138
114
  } else {
139
- reject(new Error(`Download failed with status ${response.statusCode}`));
115
+ reject(new Error(`Download failed with status ${response.statusCode} for ${url}`));
140
116
  }
141
117
  }).on('error', reject);
142
118
  });
143
119
  }
144
120
 
121
+ // ensureBinary is idempotent: it returns the path to the platform binary,
122
+ // downloading it only if it is not already present. Safe to call from both the
123
+ // postinstall hook and the runtime bin wrapper.
124
+ async function ensureBinary() {
125
+ const target = binaryPath();
126
+ if (fs.existsSync(target)) {
127
+ return target;
128
+ }
129
+ await downloadBinary();
130
+ return target;
131
+ }
132
+
145
133
  async function main() {
146
134
  try {
147
- await downloadBinary();
135
+ await ensureBinary();
148
136
  console.log('\n✓ Installation complete!');
149
137
  console.log('\nTo use with Claude Desktop, add this to your MCP configuration:');
150
138
  console.log('\nmacOS: ~/Library/Application Support/Claude/claude_desktop_config.json');
@@ -159,16 +147,20 @@ async function main() {
159
147
  console.log(' }');
160
148
  console.log('}');
161
149
  console.log('\nBefore using, configure your API keys:');
162
- console.log(' gimage auth gemini');
150
+ console.log(' gimage auth setup gemini');
163
151
  console.log('\nFor more information: https://github.com/apresai/gimage');
164
152
  } catch (error) {
165
- console.error('Installation failed:', error.message);
166
- console.error('\nFallback options:');
167
- console.error('1. Install gimage manually from: https://github.com/' + GITHUB_REPO + '/releases');
168
- console.error('2. Install via Homebrew: brew install gimage');
169
- console.error('3. Build from source: https://github.com/' + GITHUB_REPO + '#building-from-source');
170
- process.exit(1);
153
+ // Non-fatal: the bin wrapper will lazily retry the download (or fall back to
154
+ // a system gimage on PATH) on first run, so don't fail the whole install.
155
+ console.error('Postinstall could not download the gimage binary:', error.message);
156
+ console.error('It will be fetched automatically on first run, or install manually:');
157
+ console.error('1. From releases: https://github.com/' + GITHUB_REPO + '/releases');
158
+ console.error('2. Via Homebrew: brew install apresai/tap/gimage');
171
159
  }
172
160
  }
173
161
 
174
- main();
162
+ module.exports = { ensureBinary, downloadBinary, binaryPath, getPlatformInfo };
163
+
164
+ if (require.main === module) {
165
+ main();
166
+ }