@hmanlab/mmx-claude 0.4.3
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 +282 -0
- package/claude/mcp/mmx-mcp-server.ts +335 -0
- package/claude/skill/mmx/SKILL.md +69 -0
- package/dist/mmx-mcp-server.js +19850 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# `@hl-plugins/mmx-claude`
|
|
2
|
+
|
|
3
|
+
Claude Code adapter for the seven MiniMax multimodal tools. Pairs with
|
|
4
|
+
[`@hl-plugins/mmx`](../plugin-mmx/README.md) (the OpenCode plugin) — same
|
|
5
|
+
`mmx-cli` binary, same auth, two install records.
|
|
6
|
+
|
|
7
|
+
> Status: **Plan v1** — implementation below is tracked phase by phase. Update
|
|
8
|
+
> the checkboxes as each item lands. Once all boxes in a phase are checked,
|
|
9
|
+
> that phase is done and the next one can start.
|
|
10
|
+
|
|
11
|
+
## What this package is
|
|
12
|
+
|
|
13
|
+
A new sibling of `@hl-plugins/mmx` that ships the same seven tools to
|
|
14
|
+
**Claude Code** instead of OpenCode, by exposing them through a **Model
|
|
15
|
+
Context Protocol (MCP) server** that Claude Code launches at startup.
|
|
16
|
+
|
|
17
|
+
| Runtime | Package | Transport |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| OpenCode | `@hl-plugins/mmx` | `tool()` from `@opencode-ai/plugin`, runs as `.ts` |
|
|
20
|
+
| Claude Code | `@hl-plugins/mmx-claude` *(this package)* | MCP server, bundled `.js` |
|
|
21
|
+
|
|
22
|
+
The two packages share `mmx-cli` state and the MiniMax API key — only the
|
|
23
|
+
delivery mechanism differs.
|
|
24
|
+
|
|
25
|
+
## Phase index
|
|
26
|
+
|
|
27
|
+
- [Phase A — CLI contract + paths](#phase-a--cli-contract--paths)
|
|
28
|
+
- [Phase B — New package + MCP server](#phase-b--new-package--mcp-server)
|
|
29
|
+
- [Phase C — Install / uninstall / status + docs](#phase-c--install--uninstall--status--docs)
|
|
30
|
+
|
|
31
|
+
Each phase is independently shippable and demoable. Stopping after any
|
|
32
|
+
phase leaves the tree green; only the matching acceptance criteria need to
|
|
33
|
+
hold at that point.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Phase A — CLI contract + paths
|
|
38
|
+
|
|
39
|
+
**Scope.** Extend the `hl-plugins` manifest type with the two new Claude
|
|
40
|
+
fields, add the Claude-side path helpers, ship zero install behavior yet.
|
|
41
|
+
Every later phase depends on these types and helpers.
|
|
42
|
+
|
|
43
|
+
### Acceptance criteria
|
|
44
|
+
|
|
45
|
+
- [ ] `packages/cli/src/lib/registry.ts` exports `claudeMcp?: string` and
|
|
46
|
+
`claudeSkill?: string` on `PluginManifest.hlPlugins`
|
|
47
|
+
- [ ] `packages/cli/src/lib/paths.ts` exports:
|
|
48
|
+
- [ ] `claudeConfigDir()` — returns the Claude Code config directory
|
|
49
|
+
per platform (`~/.claude/` on macOS/Linux, `%APPDATA%\Claude\`
|
|
50
|
+
on Windows)
|
|
51
|
+
- [ ] `claudeConfigFile()` — returns the path to `~/.claude.json`
|
|
52
|
+
- [ ] `hlPluginsDataDir()` — returns `~/.local/share/hl-plugins/`
|
|
53
|
+
(XDG-style; platform-correct on Windows)
|
|
54
|
+
- [ ] Existing `hl-plugins list` still works and shows the existing
|
|
55
|
+
`mmx` plugin unchanged
|
|
56
|
+
- [ ] `npm run typecheck` passes
|
|
57
|
+
- [ ] New unit tests under `packages/cli/test/lib/paths.test.ts` cover
|
|
58
|
+
`claudeConfigDir`, `claudeConfigFile`, `hlPluginsDataDir` for
|
|
59
|
+
macOS / Linux / Windows path conventions
|
|
60
|
+
|
|
61
|
+
### Files touched
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
M packages/cli/src/lib/registry.ts
|
|
65
|
+
M packages/cli/src/lib/paths.ts
|
|
66
|
+
A packages/cli/test/lib/paths.test.ts
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Demo
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm run typecheck
|
|
73
|
+
node packages/cli/bin/hl-plugins.js list
|
|
74
|
+
# expected: shows @hl-plugins/mmx, no mmx-claude yet
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Phase B — New package + MCP server
|
|
80
|
+
|
|
81
|
+
**Scope.** Create `packages/plugin-mmx-claude/` from scratch. The MCP server
|
|
82
|
+
source registers the seven mmx tools backed by `src/lib.ts`'s `runMmx`. The
|
|
83
|
+
package's `package.json` declares the full `hl-plugins` contract. A
|
|
84
|
+
`bun build` script bundles the server into `dist/mmx-mcp-server.js`.
|
|
85
|
+
|
|
86
|
+
### Acceptance criteria
|
|
87
|
+
|
|
88
|
+
#### Package scaffolding
|
|
89
|
+
|
|
90
|
+
- [ ] `packages/plugin-mmx-claude/package.json` exists with `private: true`,
|
|
91
|
+
name `@hl-plugins/mmx-claude`, and a full `hl-plugins` contract:
|
|
92
|
+
- [ ] `opencodePlugin` / `opencodeSkill` omitted (not an OpenCode plugin)
|
|
93
|
+
- [ ] `claudeMcp: "./dist/mmx-mcp-server.js"`
|
|
94
|
+
- [ ] `claudeSkill: "./claude/skill/mmx/SKILL.md"`
|
|
95
|
+
- [ ] `requires` includes `mmx-cli` and `bun`
|
|
96
|
+
- [ ] `auth` block carried over from `@hl-plugins/mmx`
|
|
97
|
+
- [ ] `packages/plugin-mmx-claude/tsconfig.json` extends the base config
|
|
98
|
+
- [ ] `packages/plugin-mmx-claude/bunfig.toml` is present
|
|
99
|
+
- [ ] Workspace `package.json` lists the new package under `workspaces`
|
|
100
|
+
|
|
101
|
+
#### Source files
|
|
102
|
+
|
|
103
|
+
- [ ] `src/lib.ts` exports `runMmx`, `resolveOutDir`, and the
|
|
104
|
+
suspicious-path detector (~50 lines, `Bun.spawn`)
|
|
105
|
+
- [ ] `claude/mcp/mmx-mcp-server.ts` registers the seven tools via the
|
|
106
|
+
`@modelcontextprotocol/sdk` Server class:
|
|
107
|
+
- [ ] `mmx_image`
|
|
108
|
+
- [ ] `mmx_speech`
|
|
109
|
+
- [ ] `mmx_video`
|
|
110
|
+
- [ ] `mmx_music`
|
|
111
|
+
- [ ] `mmx_search`
|
|
112
|
+
- [ ] `mmx_vision`
|
|
113
|
+
- [ ] `mmx_quota`
|
|
114
|
+
- [ ] `claude/skill/mmx/SKILL.md` mirrors the OpenCode skill's content,
|
|
115
|
+
edited for Claude Code context (no `@opencode-ai/plugin` references)
|
|
116
|
+
|
|
117
|
+
#### Build
|
|
118
|
+
|
|
119
|
+
- [ ] `bun run --filter @hl-plugins/mmx-claude build` produces
|
|
120
|
+
`packages/plugin-mmx-claude/dist/mmx-mcp-server.js`
|
|
121
|
+
- [ ] `dist/` is listed in `.gitignore`
|
|
122
|
+
- [ ] `npm run typecheck` passes across the workspace
|
|
123
|
+
|
|
124
|
+
#### Tests
|
|
125
|
+
|
|
126
|
+
- [ ] `test/lib.test.ts` covers:
|
|
127
|
+
- [ ] Suspicious-path rejection (HOME, ~/Desktop, /tmp, `.`, `..`)
|
|
128
|
+
- [ ] `MMX_OUTPUT_DIR` env-var override wins over the default
|
|
129
|
+
- [ ] Default `~/Desktop/mmx-output/` resolution
|
|
130
|
+
- [ ] `test/mcp-smoke.test.ts` spawns the bundle over stdio, sends a
|
|
131
|
+
JSON-RPC `tools/list` request, and asserts all seven tool names
|
|
132
|
+
appear in the response
|
|
133
|
+
|
|
134
|
+
### Files touched
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
A packages/plugin-mmx-claude/package.json
|
|
138
|
+
A packages/plugin-mmx-claude/tsconfig.json
|
|
139
|
+
A packages/plugin-mmx-claude/bunfig.toml
|
|
140
|
+
A packages/plugin-mmx-claude/src/lib.ts
|
|
141
|
+
A packages/plugin-mmx-claude/claude/mcp/mmx-mcp-server.ts
|
|
142
|
+
A packages/plugin-mmx-claude/claude/skill/mmx/SKILL.md
|
|
143
|
+
A packages/plugin-mmx-claude/test/lib.test.ts
|
|
144
|
+
A packages/plugin-mmx-claude/test/mcp-smoke.test.ts
|
|
145
|
+
M package.json # add @modelcontextprotocol/sdk devDep
|
|
146
|
+
M packages/plugin-mmx-claude/.gitignore # OR root .gitignore (dist)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Demo
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
bun run --filter @hl-plugins/mmx-claude build
|
|
153
|
+
node packages/plugin-mmx-claude/test/mcp-smoke.test.ts
|
|
154
|
+
# expected: stdout lists mmx_image, mmx_speech, mmx_video, mmx_music,
|
|
155
|
+
# mmx_search, mmx_vision, mmx_quota
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Phase C — Install / uninstall / status + docs
|
|
161
|
+
|
|
162
|
+
**Scope.** Wire the new contract fields into the install flow. Add
|
|
163
|
+
`addMcpServer` / `removeMcpServer` helpers. Update status to report
|
|
164
|
+
Claude-side state. Add the install hint for Claude Code systems. Update
|
|
165
|
+
`README.md`, `docs/architecture.md`, and `docs/adding-a-plugin.md`.
|
|
166
|
+
|
|
167
|
+
### Acceptance criteria
|
|
168
|
+
|
|
169
|
+
#### Helpers
|
|
170
|
+
|
|
171
|
+
- [ ] `packages/cli/src/lib/config.ts` exports `addMcpServer(name, spec)`
|
|
172
|
+
and `removeMcpServer(name)` — defensive `~/.claude.json` parsing
|
|
173
|
+
with a clear error if the file is unrecognizable
|
|
174
|
+
- [ ] `packages/cli/src/commands/install.ts` extended:
|
|
175
|
+
- [ ] Copies the `claudeMcp` bundle to
|
|
176
|
+
`~/.local/share/hl-plugins/<plugin>/<file>` and updates the
|
|
177
|
+
bundle's on-disk path in the MCP spec before merging
|
|
178
|
+
- [ ] Copies the `claudeSkill` markdown to
|
|
179
|
+
`~/.claude/skills/<plugin>/SKILL.md`
|
|
180
|
+
- [ ] Calls `addMcpServer` to merge the spec into `~/.claude.json`
|
|
181
|
+
- [ ] All three steps are idempotent (same merge semantics as the
|
|
182
|
+
OpenCode path)
|
|
183
|
+
- [ ] `packages/cli/src/commands/uninstall.ts` extended:
|
|
184
|
+
- [ ] Removes the bundle from `~/.local/share/hl-plugins/<plugin>/`
|
|
185
|
+
- [ ] Removes the skill from `~/.claude/skills/<plugin>/`
|
|
186
|
+
- [ ] Calls `removeMcpServer` to drop the entry from `~/.claude.json`
|
|
187
|
+
- [ ] `packages/cli/src/commands/status.ts` extended to report all five
|
|
188
|
+
Claude-side install points per plugin as present/missing
|
|
189
|
+
- [ ] Install hint: when `hl-plugins install mmx` runs on a system with
|
|
190
|
+
`~/.claude/` present but no `~/.opencode/`, prints a hint about
|
|
191
|
+
`mmx-claude` and proceeds with the OpenCode install
|
|
192
|
+
|
|
193
|
+
#### Documentation
|
|
194
|
+
|
|
195
|
+
- [ ] `README.md` — split the plugin table row into two
|
|
196
|
+
(OpenCode + Claude Code), add a "Choosing the right package"
|
|
197
|
+
one-liner
|
|
198
|
+
- [ ] `docs/architecture.md` — document `claudeMcp` + `claudeSkill` in
|
|
199
|
+
the contract section; extend the install-flow diagram with a
|
|
200
|
+
Claude Code branch
|
|
201
|
+
- [ ] `docs/adding-a-plugin.md` — note the four contract fields
|
|
202
|
+
(`opencodePlugin`, `opencodeSkill`, `claudeMcp`, `claudeSkill`) and
|
|
203
|
+
that a plugin can declare any subset; add a Claude Code example
|
|
204
|
+
|
|
205
|
+
#### End-to-end smoke
|
|
206
|
+
|
|
207
|
+
- [ ] `hl-plugins install mmx-claude` on a clean Claude Code system:
|
|
208
|
+
- [ ] Auto-installs Bun if missing (via the `requires` entry)
|
|
209
|
+
- [ ] Auto-installs `mmx-cli` if missing
|
|
210
|
+
- [ ] Prompts for API key (or reads `MMX_API_KEY`)
|
|
211
|
+
- [ ] Copies the MCP bundle to
|
|
212
|
+
`~/.local/share/hl-plugins/mmx-claude/mmx-mcp-server.js`
|
|
213
|
+
- [ ] Copies the skill MD to `~/.claude/skills/mmx-claude/SKILL.md`
|
|
214
|
+
- [ ] Merges `mcpServers.mmx-claude` into `~/.claude.json`
|
|
215
|
+
- [ ] Runs `mmx quota` as the post-install smoke test
|
|
216
|
+
- [ ] `hl-plugins uninstall mmx-claude` reverses every step above
|
|
217
|
+
- [ ] `hl-plugins status mmx-claude` reports all five install points
|
|
218
|
+
- [ ] `hl-plugins install mmx` on a system with `~/.claude/` but no
|
|
219
|
+
`~/.opencode/` prints the hint and proceeds
|
|
220
|
+
- [ ] Re-running `hl-plugins install mmx-claude` is a no-op (idempotent)
|
|
221
|
+
- [ ] `npm run typecheck` + `npm run build` both green
|
|
222
|
+
|
|
223
|
+
### Files touched
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
M packages/cli/src/lib/config.ts
|
|
227
|
+
M packages/cli/src/commands/install.ts
|
|
228
|
+
M packages/cli/src/commands/uninstall.ts
|
|
229
|
+
M packages/cli/src/commands/status.ts
|
|
230
|
+
M README.md
|
|
231
|
+
M docs/architecture.md
|
|
232
|
+
M docs/adding-a-plugin.md
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Demo
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
node packages/cli/bin/hl-plugins.js install mmx-claude
|
|
239
|
+
# expected: Bun auto-installs (if missing) -> mmx-cli auto-installs (if
|
|
240
|
+
# missing) -> API key prompt -> bundle copied -> skill copied
|
|
241
|
+
# -> ~/.claude.json merged -> mmx quota smoke test -> green
|
|
242
|
+
# checkmarks per step
|
|
243
|
+
|
|
244
|
+
node packages/cli/bin/hl-plugins.js status mmx-claude
|
|
245
|
+
# expected: five green checks (bundle, skill, mcpServers entry,
|
|
246
|
+
# auth present, mmx quota responds)
|
|
247
|
+
|
|
248
|
+
node packages/cli/bin/hl-plugins.js uninstall mmx-claude
|
|
249
|
+
# expected: every install step reversed, exit 0
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Cross-phase checklist
|
|
255
|
+
|
|
256
|
+
These items are not tied to one phase; they belong to the whole feature
|
|
257
|
+
and should land before merge.
|
|
258
|
+
|
|
259
|
+
- [ ] `packages/plugin-mmx` source is **untouched** — zero edits, the
|
|
260
|
+
OpenCode plugin stays as-is
|
|
261
|
+
- [ ] Both packages stay `private: true` until the install flow is
|
|
262
|
+
battle-tested on real Claude Code installs
|
|
263
|
+
- [ ] `~/Desktop/mmx-output/` is the default output directory in both
|
|
264
|
+
packages; `MMX_OUTPUT_DIR` overrides it in both
|
|
265
|
+
- [ ] Same suspicious-path rules apply in both packages (no writes to
|
|
266
|
+
HOME, ~/Desktop, /tmp, or `.`)
|
|
267
|
+
- [ ] No secrets committed; `MMX_API_KEY` is read from env, never echoed
|
|
268
|
+
- [ ] All `.ts` strict-mode clean; `npm run typecheck` green across the
|
|
269
|
+
workspace
|
|
270
|
+
- [ ] No npm publish from the agent — the human maintainer runs
|
|
271
|
+
`npm run publish:cli`
|
|
272
|
+
|
|
273
|
+
## Out of scope (deferred)
|
|
274
|
+
|
|
275
|
+
- Slash commands (`/mmx-image ...`) — could ship in a v2 alongside the
|
|
276
|
+
MCP server for explicit user-driven invocation
|
|
277
|
+
- Auto-update of the MCP bundle — `hl-plugins update` will need a
|
|
278
|
+
Claude-side mirror; tracked separately
|
|
279
|
+
- Cross-agent plugin state sync — the two agents share `mmx-cli` state,
|
|
280
|
+
nothing else
|
|
281
|
+
- Publishing either package to npm — both stay `private: true` until the
|
|
282
|
+
install flow is battle-tested
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// mmx-mcp-server.ts
|
|
2
|
+
//
|
|
3
|
+
// MCP server that exposes the seven mmx tools to Claude Code. Loaded
|
|
4
|
+
// at startup by Claude Code's mcpServers config; communicates over
|
|
5
|
+
// stdio via JSON-RPC.
|
|
6
|
+
//
|
|
7
|
+
// Built with: bun build ./claude/mcp/mmx-mcp-server.ts --target=bun
|
|
8
|
+
// --outfile=./dist/mmx-mcp-server.js
|
|
9
|
+
//
|
|
10
|
+
// All generated files default to ~/Desktop/mmx-output/. The user can
|
|
11
|
+
// override the default directory permanently via $MMX_OUTPUT_DIR; the
|
|
12
|
+
// LLM should never pass out_dir / out_path unless the user explicitly
|
|
13
|
+
// asked.
|
|
14
|
+
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
|
16
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
17
|
+
// Import from `zod/v3` so the type identity matches the SDK's internal
|
|
18
|
+
// zod-compat. Without this, the SDK's AnySchema = z3.ZodTypeAny | z4.$ZodType
|
|
19
|
+
// rejects the zod `ZodString` we hand in (TS sees two distinct ZodString
|
|
20
|
+
// types because they were loaded from different module specifiers).
|
|
21
|
+
import { z } from "zod/v3"
|
|
22
|
+
import { dirname } from "node:path"
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_OUT_DIR,
|
|
25
|
+
ensureDir,
|
|
26
|
+
resolveFilePath,
|
|
27
|
+
resolveOutDir,
|
|
28
|
+
runMmx,
|
|
29
|
+
warnSuspiciousOutDir,
|
|
30
|
+
} from "../../src/lib.js"
|
|
31
|
+
|
|
32
|
+
const server = new McpServer({
|
|
33
|
+
name: "mmx-claude",
|
|
34
|
+
version: "0.1.0",
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Claude Code does not have a worktree concept the way OpenCode does.
|
|
38
|
+
// process.cwd() is the closest analogue — it's the directory Claude was
|
|
39
|
+
// launched from, which is what relative paths in tool args would resolve
|
|
40
|
+
// against. The OpenCode plugin uses ctx.worktree; we use cwd() here.
|
|
41
|
+
const worktree = process.cwd()
|
|
42
|
+
|
|
43
|
+
function textResult(text: string) {
|
|
44
|
+
return { content: [{ type: "text" as const, text }] }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── mmx_image ──────────────────────────────────────────────────────────
|
|
48
|
+
server.registerTool(
|
|
49
|
+
"mmx_image",
|
|
50
|
+
{
|
|
51
|
+
description:
|
|
52
|
+
"Generate one or more images from a text prompt using MiniMax's image-01 model via mmx-cli. Use this whenever the user asks for an image, illustration, logo, artwork, photo, or any visual asset. Aspect ratios: 1:1, 16:9, 9:16, 4:3, 3:4, 21:9. Pass seed for reproducible results. Returns the saved file path.",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
prompt: z
|
|
55
|
+
.string()
|
|
56
|
+
.describe(
|
|
57
|
+
"Detailed text description of the image. Be specific about subject, style, lighting, composition, mood.",
|
|
58
|
+
),
|
|
59
|
+
aspect_ratio: z
|
|
60
|
+
.string()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe("Aspect ratio. One of: 1:1, 16:9, 9:16, 4:3, 3:4, 21:9. Default 1:1."),
|
|
63
|
+
n: z.number().optional().describe("Number of images to generate. Default 1, max 4."),
|
|
64
|
+
seed: z.number().optional().describe("Random seed for reproducible generation."),
|
|
65
|
+
out_dir: z
|
|
66
|
+
.string()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe(
|
|
69
|
+
"Override save directory. Leave unset unless the user explicitly asked for a different save location in this conversation. Default: ~/Desktop/mmx-output/ (or $MMX_OUTPUT_DIR if set). Suspicious paths fall back to the default with a warning.",
|
|
70
|
+
),
|
|
71
|
+
optimize_prompt: z.boolean().optional().describe("Auto-optimize the prompt for better quality."),
|
|
72
|
+
filename_prefix: z
|
|
73
|
+
.string()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe(
|
|
76
|
+
"Filename prefix. Defaults to a unique per-call value (image-<timestamp>) so back-to-back calls don't overwrite each other. Pass an explicit value for predictable sequential naming.",
|
|
77
|
+
),
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
async (args) => {
|
|
81
|
+
const requestedDir = resolveOutDir(args.out_dir, worktree)
|
|
82
|
+
const outDir = requestedDir === "" || requestedDir === "." ? DEFAULT_OUT_DIR : requestedDir
|
|
83
|
+
ensureDir(outDir)
|
|
84
|
+
const filenamePrefix = args.filename_prefix ?? `image-${Date.now()}`
|
|
85
|
+
const cliArgs = ["image", "generate", "--prompt", args.prompt]
|
|
86
|
+
if (args.aspect_ratio) cliArgs.push("--aspect-ratio", args.aspect_ratio)
|
|
87
|
+
if (args.n) cliArgs.push("--n", String(args.n))
|
|
88
|
+
if (args.seed != null) cliArgs.push("--seed", String(args.seed))
|
|
89
|
+
if (args.optimize_prompt) cliArgs.push("--prompt-optimizer")
|
|
90
|
+
cliArgs.push("--out-dir", outDir, "--out-prefix", filenamePrefix, "--non-interactive")
|
|
91
|
+
const { stdout, stderr, exitCode } = await runMmx(cliArgs)
|
|
92
|
+
if (exitCode !== 0) {
|
|
93
|
+
return textResult(`mmx image generate failed (exit ${exitCode}):\n${stderr || stdout || "(no output)"}`)
|
|
94
|
+
}
|
|
95
|
+
let msg = `Image generation complete.\n\n${stdout.trim()}\n\nSaved to: ${outDir}\nFilename prefix: ${filenamePrefix}`
|
|
96
|
+
if (outDir !== requestedDir && args.out_dir) {
|
|
97
|
+
msg += `\n\n${warnSuspiciousOutDir(args.out_dir, outDir)}`
|
|
98
|
+
}
|
|
99
|
+
return textResult(msg)
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// ─── mmx_speech ─────────────────────────────────────────────────────────
|
|
104
|
+
server.registerTool(
|
|
105
|
+
"mmx_speech",
|
|
106
|
+
{
|
|
107
|
+
description:
|
|
108
|
+
"Synthesize speech from text using MiniMax's speech-2.8-hd model. Use when the user wants a voiceover, narration, audio file, TTS, or to read text aloud. Saves an MP3 and returns the path. 40+ languages. Default voice: English_expressive_narrator.",
|
|
109
|
+
inputSchema: {
|
|
110
|
+
text: z.string().describe("The text to speak. Up to 10,000 characters."),
|
|
111
|
+
voice: z.string().optional().describe("Voice ID. Default English_expressive_narrator."),
|
|
112
|
+
speed: z.number().optional().describe("Speech speed multiplier. Default 1.0."),
|
|
113
|
+
out_path: z
|
|
114
|
+
.string()
|
|
115
|
+
.optional()
|
|
116
|
+
.describe(
|
|
117
|
+
"Override output .mp3 path. Leave unset unless the user explicitly asked for a different save location in this conversation. Default: ~/Desktop/mmx-output/speech-<timestamp>.mp3 (or $MMX_OUTPUT_DIR if set). Suspicious parent directories fall back to the default with a warning.",
|
|
118
|
+
),
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
async (args) => {
|
|
122
|
+
const {
|
|
123
|
+
filePath: outPath,
|
|
124
|
+
wasSuspicious,
|
|
125
|
+
originalArg,
|
|
126
|
+
} = resolveFilePath(args.out_path, worktree, `speech-${Date.now()}.mp3`)
|
|
127
|
+
ensureDir(dirname(outPath))
|
|
128
|
+
const cliArgs = ["speech", "synthesize", "--text", args.text]
|
|
129
|
+
if (args.voice) cliArgs.push("--voice", args.voice)
|
|
130
|
+
if (args.speed != null) cliArgs.push("--speed", String(args.speed))
|
|
131
|
+
cliArgs.push("--out", outPath, "--non-interactive")
|
|
132
|
+
const { stdout, stderr, exitCode } = await runMmx(cliArgs)
|
|
133
|
+
if (exitCode !== 0) {
|
|
134
|
+
return textResult(
|
|
135
|
+
`mmx speech synthesize failed (exit ${exitCode}):\n${stderr || stdout || "(no output)"}`,
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
let msg = `Speech synthesized.\n\nSaved to: ${outPath}`
|
|
139
|
+
if (wasSuspicious && originalArg) {
|
|
140
|
+
msg += `\n\n${warnSuspiciousOutDir(originalArg, outPath)}`
|
|
141
|
+
}
|
|
142
|
+
return textResult(msg)
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
// ─── mmx_video ──────────────────────────────────────────────────────────
|
|
147
|
+
server.registerTool(
|
|
148
|
+
"mmx_video",
|
|
149
|
+
{
|
|
150
|
+
description:
|
|
151
|
+
"Generate a short video from a text prompt using MiniMax's Hailuo-2.3 model. Use when the user wants a video clip, animation, or motion graphic. Generation can take 1-3 minutes. Returns the output MP4 file path.",
|
|
152
|
+
inputSchema: {
|
|
153
|
+
prompt: z
|
|
154
|
+
.string()
|
|
155
|
+
.describe("Detailed description of the video scene, including camera movement and action."),
|
|
156
|
+
model: z
|
|
157
|
+
.string()
|
|
158
|
+
.optional()
|
|
159
|
+
.describe(
|
|
160
|
+
"Model ID. Default MiniMax-Hailuo-2.3. Use MiniMax-Hailuo-2.3-Fast for quicker lower-quality results.",
|
|
161
|
+
),
|
|
162
|
+
out_path: z
|
|
163
|
+
.string()
|
|
164
|
+
.optional()
|
|
165
|
+
.describe(
|
|
166
|
+
"Override output .mp4 path. Leave unset unless the user explicitly asked for a different save location in this conversation. Default: ~/Desktop/mmx-output/video-<timestamp>.mp4 (or $MMX_OUTPUT_DIR if set). Suspicious parent directories fall back to the default with a warning.",
|
|
167
|
+
),
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
async (args) => {
|
|
171
|
+
const {
|
|
172
|
+
filePath: outPath,
|
|
173
|
+
wasSuspicious,
|
|
174
|
+
originalArg,
|
|
175
|
+
} = resolveFilePath(args.out_path, worktree, `video-${Date.now()}.mp4`)
|
|
176
|
+
ensureDir(dirname(outPath))
|
|
177
|
+
const cliArgs = ["video", "generate", "--prompt", args.prompt]
|
|
178
|
+
if (args.model) cliArgs.push("--model", args.model)
|
|
179
|
+
cliArgs.push("--download", outPath, "--non-interactive")
|
|
180
|
+
const { stdout, stderr, exitCode } = await runMmx(cliArgs)
|
|
181
|
+
if (exitCode !== 0) {
|
|
182
|
+
return textResult(`mmx video generate failed (exit ${exitCode}):\n${stderr || stdout || "(no output)"}`)
|
|
183
|
+
}
|
|
184
|
+
let msg = `Video generation complete.\n\nSaved to: ${outPath}`
|
|
185
|
+
if (wasSuspicious && originalArg) {
|
|
186
|
+
msg += `\n\n${warnSuspiciousOutDir(originalArg, outPath)}`
|
|
187
|
+
}
|
|
188
|
+
return textResult(msg)
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
// ─── mmx_music ──────────────────────────────────────────────────────────
|
|
193
|
+
server.registerTool(
|
|
194
|
+
"mmx_music",
|
|
195
|
+
{
|
|
196
|
+
description:
|
|
197
|
+
"Generate a song or instrumental music from a style prompt using MiniMax's music-2.6 model. Use when the user wants background music, a theme song, a jingle, or instrumental music. Either supply lyrics or set instrumental=true.",
|
|
198
|
+
inputSchema: {
|
|
199
|
+
prompt: z
|
|
200
|
+
.string()
|
|
201
|
+
.describe(
|
|
202
|
+
"Style description: genre, mood, instruments, tempo. E.g. 'cinematic orchestral, building tension'.",
|
|
203
|
+
),
|
|
204
|
+
lyrics: z
|
|
205
|
+
.string()
|
|
206
|
+
.optional()
|
|
207
|
+
.describe("Song lyrics with structure tags like [Verse], [Chorus]. Omit for instrumental."),
|
|
208
|
+
instrumental: z.boolean().optional().describe("If true, generate instrumental music with no vocals."),
|
|
209
|
+
vocals: z.string().optional().describe("Vocal style hint, e.g. 'warm male baritone'."),
|
|
210
|
+
bpm: z.number().optional().describe("Exact tempo in BPM."),
|
|
211
|
+
out_path: z
|
|
212
|
+
.string()
|
|
213
|
+
.optional()
|
|
214
|
+
.describe(
|
|
215
|
+
"Override output .mp3 path. Leave unset unless the user explicitly asked for a different save location in this conversation. Default: ~/Desktop/mmx-output/music-<timestamp>.mp3 (or $MMX_OUTPUT_DIR if set). Suspicious parent directories fall back to the default with a warning.",
|
|
216
|
+
),
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
async (args) => {
|
|
220
|
+
const {
|
|
221
|
+
filePath: outPath,
|
|
222
|
+
wasSuspicious,
|
|
223
|
+
originalArg,
|
|
224
|
+
} = resolveFilePath(args.out_path, worktree, `music-${Date.now()}.mp3`)
|
|
225
|
+
ensureDir(dirname(outPath))
|
|
226
|
+
const cliArgs = ["music", "generate", "--prompt", args.prompt]
|
|
227
|
+
if (args.lyrics) cliArgs.push("--lyrics", args.lyrics)
|
|
228
|
+
if (args.instrumental) cliArgs.push("--instrumental")
|
|
229
|
+
if (args.vocals) cliArgs.push("--vocals", args.vocals)
|
|
230
|
+
if (args.bpm != null) cliArgs.push("--bpm", String(args.bpm))
|
|
231
|
+
cliArgs.push("--out", outPath, "--non-interactive")
|
|
232
|
+
const { stdout, stderr, exitCode } = await runMmx(cliArgs)
|
|
233
|
+
if (exitCode !== 0) {
|
|
234
|
+
return textResult(`mmx music generate failed (exit ${exitCode}):\n${stderr || stdout || "(no output)"}`)
|
|
235
|
+
}
|
|
236
|
+
let msg = `Music generation complete.\n\nSaved to: ${outPath}`
|
|
237
|
+
if (wasSuspicious && originalArg) {
|
|
238
|
+
msg += `\n\n${warnSuspiciousOutDir(originalArg, outPath)}`
|
|
239
|
+
}
|
|
240
|
+
return textResult(msg)
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
// ─── mmx_search ─────────────────────────────────────────────────────────
|
|
245
|
+
server.registerTool(
|
|
246
|
+
"mmx_search",
|
|
247
|
+
{
|
|
248
|
+
description:
|
|
249
|
+
"Search the web using MiniMax's search API. Use when the user wants current information, news, facts, or anything time-sensitive. Returns a textual summary of search results.",
|
|
250
|
+
inputSchema: {
|
|
251
|
+
query: z.string().describe("The search query."),
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
async (args) => {
|
|
255
|
+
const { stdout, stderr, exitCode } = await runMmx([
|
|
256
|
+
"search",
|
|
257
|
+
"query",
|
|
258
|
+
"--q",
|
|
259
|
+
args.query,
|
|
260
|
+
"--output",
|
|
261
|
+
"json",
|
|
262
|
+
"--non-interactive",
|
|
263
|
+
])
|
|
264
|
+
if (exitCode !== 0) {
|
|
265
|
+
return textResult(`mmx search failed (exit ${exitCode}):\n${stderr || stdout || "(no output)"}`)
|
|
266
|
+
}
|
|
267
|
+
return textResult(stdout.trim() || "(no results)")
|
|
268
|
+
},
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
// ─── mmx_vision ─────────────────────────────────────────────────────────
|
|
272
|
+
server.registerTool(
|
|
273
|
+
"mmx_vision",
|
|
274
|
+
{
|
|
275
|
+
description:
|
|
276
|
+
"Describe or analyze an image using MiniMax's vision model. Pass a local file path or URL. Returns a textual description. Useful when the user uploads an image and wants analysis, OCR, or a description.",
|
|
277
|
+
inputSchema: {
|
|
278
|
+
image: z.string().describe("Local file path or URL of the image."),
|
|
279
|
+
prompt: z
|
|
280
|
+
.string()
|
|
281
|
+
.optional()
|
|
282
|
+
.describe("Custom question about the image. Default 'Describe the image.'"),
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
async (args) => {
|
|
286
|
+
const cliArgs = ["vision", "describe", "--image", args.image]
|
|
287
|
+
if (args.prompt) cliArgs.push("--prompt", args.prompt)
|
|
288
|
+
cliArgs.push("--non-interactive")
|
|
289
|
+
const { stdout, stderr, exitCode } = await runMmx(cliArgs)
|
|
290
|
+
if (exitCode !== 0) {
|
|
291
|
+
return textResult(
|
|
292
|
+
`mmx vision describe failed (exit ${exitCode}):\n${stderr || stdout || "(no output)"}`,
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
return textResult(stdout.trim() || "(no description)")
|
|
296
|
+
},
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
// ─── mmx_quota ──────────────────────────────────────────────────────────
|
|
300
|
+
server.registerTool(
|
|
301
|
+
"mmx_quota",
|
|
302
|
+
{
|
|
303
|
+
description:
|
|
304
|
+
"Show current Token Plan usage and remaining quota (5-hour rolling and weekly windows). Use when the user asks about quota, usage, limits, or how many calls they have left.",
|
|
305
|
+
},
|
|
306
|
+
async () => {
|
|
307
|
+
let { stdout, stderr, exitCode } = await runMmx(["quota"])
|
|
308
|
+
if (exitCode !== 0) {
|
|
309
|
+
const fallback = await runMmx(["quota", "show", "--non-interactive"])
|
|
310
|
+
if (fallback.exitCode === 0) {
|
|
311
|
+
stdout = fallback.stdout
|
|
312
|
+
stderr = fallback.stderr
|
|
313
|
+
exitCode = fallback.exitCode
|
|
314
|
+
} else {
|
|
315
|
+
return textResult(`mmx quota failed (exit ${exitCode}):\n${stderr || "(no stderr)"}`)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const raw = stdout.trim()
|
|
319
|
+
let data: any
|
|
320
|
+
try {
|
|
321
|
+
data = JSON.parse(raw)
|
|
322
|
+
} catch {
|
|
323
|
+
return textResult(raw || "(no quota info)")
|
|
324
|
+
}
|
|
325
|
+
const rows = (data.model_remains ?? []).map((m: any) => {
|
|
326
|
+
const reset = new Date(m.end_time).toUTCString().replace(/^[^,]+,\s*/, "")
|
|
327
|
+
return `| ${m.model_name} | ${m.current_interval_remaining_percent}% left | ${m.current_weekly_remaining_percent}% left | resets ${reset} |`
|
|
328
|
+
})
|
|
329
|
+
const header = "| Model | 5h window | Weekly | Next reset |\n|---|---|---|---|"
|
|
330
|
+
return textResult(`Quota — Token Plan\n\n${header}\n${rows.join("\n")}\n`)
|
|
331
|
+
},
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
const transport = new StdioServerTransport()
|
|
335
|
+
await server.connect(transport)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mmx
|
|
3
|
+
description: Use when the user wants to generate images, video, music, speech, run web search, analyze images, or check Token Plan quota. Wraps the official mmx-cli so multimodal MiniMax capabilities can be invoked directly from inside a Claude Code session without leaving the chat. Front-load keywords: image, picture, illustration, logo, artwork, generate, mmx, MiniMax, video, music, song, voiceover, TTS, speech, search, vision, quota, usage.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# mmx — MiniMax multimodal tools
|
|
7
|
+
|
|
8
|
+
The `mmx-claude` MCP server exposes seven tools that wrap the official MiniMax CLI. They are auto-registered when the user runs `hl-plugins install mmx-claude` and Claude Code restarts.
|
|
9
|
+
|
|
10
|
+
| Tool | What it does |
|
|
11
|
+
| ----------------- | ---------------------------------------------------- |
|
|
12
|
+
| `mmx_image` | Text → image (image-01 model) |
|
|
13
|
+
| `mmx_speech` | Text → MP3 voiceover (speech-2.8-hd) |
|
|
14
|
+
| `mmx_video` | Text → MP4 short video (Hailuo-2.3) |
|
|
15
|
+
| `mmx_music` | Text → song / instrumental (music-2.6) |
|
|
16
|
+
| `mmx_search` | Web search via MiniMax search API |
|
|
17
|
+
| `mmx_vision` | Image → text description / OCR / Q&A |
|
|
18
|
+
| `mmx_quota` | Show Token Plan usage and remaining quota |
|
|
19
|
+
|
|
20
|
+
These are the same seven tools available to OpenCode users via `@hl-plugins/mmx`. Same `mmx-cli` binary, same auth state, same MiniMax API key.
|
|
21
|
+
|
|
22
|
+
## Setup (one-time, on the machine)
|
|
23
|
+
|
|
24
|
+
1. Install the CLI: `npm install -g mmx-cli`
|
|
25
|
+
2. Install Bun (the MCP server runs on it): `curl -fsSL https://bun.sh/install | bash`
|
|
26
|
+
3. Authenticate with the user's API key: `mmx auth login --api-key sk-xxxxx`
|
|
27
|
+
The key is stored locally — never paste it into chat.
|
|
28
|
+
4. If calls return 401, the region auto-detect failed. Set it manually:
|
|
29
|
+
- Overseas: `mmx config set --key region --value global`
|
|
30
|
+
- Mainland China: `mmx config set --key region --value cn`
|
|
31
|
+
5. Confirm with: `mmx quota` and `mmx auth status`
|
|
32
|
+
|
|
33
|
+
The user runs `hl-plugins install mmx-claude` once and the CLI handles steps 1–4 automatically (auto-installing Bun and `mmx-cli` if missing).
|
|
34
|
+
|
|
35
|
+
## Output location
|
|
36
|
+
|
|
37
|
+
All generated files save to `~/Desktop/mmx-output/` by default — regardless of which repo or directory Claude Code was launched from. This is intentional; do not override it on your own.
|
|
38
|
+
|
|
39
|
+
**Never pass `out_dir` / `out_path` unless the user explicitly asked for a different save location in this conversation.** Passing these args based on your own inference (e.g. "save to the current repo") is wrong and will be silently corrected to the default with a warning in the tool output.
|
|
40
|
+
|
|
41
|
+
Two ways the user can legitimately override:
|
|
42
|
+
|
|
43
|
+
1. **Per-call:** tell you explicitly in chat ("save this to my Pictures folder"), and you pass the right `out_dir` / `out_path`.
|
|
44
|
+
2. **Permanent:** set `MMX_OUTPUT_DIR=/some/path` in the user's shell rc (`~/.zshrc`, `~/.bashrc`) before launching Claude Code. This changes the default for all mmx tools.
|
|
45
|
+
|
|
46
|
+
Suspicious paths (`$HOME`, `~/Desktop`, `/tmp`, `.`) passed via `out_dir` / `out_path` are always rejected and fall back to `~/Desktop/mmx-output/` — even when the user explicitly asks for them. (Users wanting those locations should use `MMX_OUTPUT_DIR` for the legitimate case.)
|
|
47
|
+
|
|
48
|
+
`mmx_image` defaults to a unique filename prefix per call (`image-<timestamp>`) so back-to-back calls don't overwrite each other. Pass an explicit `filename_prefix` when you want predictable sequential naming (e.g. `logo_001.jpg`, `logo_002.jpg`).
|
|
49
|
+
|
|
50
|
+
## Common patterns
|
|
51
|
+
|
|
52
|
+
- **Cover image for a slide / social post** — call `mmx_image` with a detailed style prompt and `aspect_ratio: "16:9"` or `"1:1"`.
|
|
53
|
+
- **Voiceover for a video** — `mmx_speech` with the script as `text`. Override voice ID if the default English narrator doesn't fit.
|
|
54
|
+
- **Background music** — `mmx_music` with `instrumental: true` and a style prompt like "calm ambient pad, lo-fi beat, 70 bpm".
|
|
55
|
+
- **Verify what's left in the quota** — `mmx_quota` before starting a long batch.
|
|
56
|
+
- **Describe a screenshot the user dropped in** — `mmx_vision` with the local path.
|
|
57
|
+
|
|
58
|
+
## Tips
|
|
59
|
+
|
|
60
|
+
- Image prompts should be **specific and detailed**: subject, style, lighting, composition, mood. Vague prompts produce vague results.
|
|
61
|
+
- For reproducible images, pass `seed`. Same prompt + same seed = same image.
|
|
62
|
+
- Video and music calls block for 1–3 minutes — call them only when the user actually wants the output.
|
|
63
|
+
- If `mmx_image` returns a failed exit code, surface the stderr verbatim — that's where mmx-cli writes quota errors, region errors, and validation issues.
|
|
64
|
+
|
|
65
|
+
## When NOT to use these tools
|
|
66
|
+
|
|
67
|
+
- For diagrams, charts, tables, or anything textual — render with HTML/JSX instead.
|
|
68
|
+
- For tiny UI icons — too heavy; use SVG.
|
|
69
|
+
- If the user is on the free tier or hasn't authenticated, point them at the setup section above before retrying.
|