meltflex-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 +71 -0
- package/dist/api.js +83 -0
- package/dist/cli.js +127 -0
- package/dist/config.js +42 -0
- package/dist/server.js +99 -0
- package/dist/util.js +39 -0
- package/package.json +49 -0
- package/skill/SKILL.md +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MeltFlex
|
|
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,71 @@
|
|
|
1
|
+
# meltflex-mcp
|
|
2
|
+
|
|
3
|
+
MeltFlex AI as an **MCP server + CLI** — generate photorealistic interior-design redesigns directly from your AI agent (Claude Code, Cursor, Codex, and any MCP-compatible client).
|
|
4
|
+
|
|
5
|
+
Each user connects **their own** MeltFlex account; every generation is billed to that account's credits. The server is a thin client over the public MeltFlex API — it enforces nothing itself, so credits and access are always governed server-side.
|
|
6
|
+
|
|
7
|
+
## Install & authenticate
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Authenticate once (stored in ~/.meltflex/config.json, chmod 600)
|
|
11
|
+
npx -y meltflex-mcp auth mf_sk_xxxxxxxxxxxx
|
|
12
|
+
npx -y meltflex-mcp whoami
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Get an API key at <https://www.meltflexai.com/settings> (Growth plan or higher).
|
|
16
|
+
Alternatively, skip `auth` and provide `MELTFLEX_API_KEY` via the client's `env` block (below).
|
|
17
|
+
|
|
18
|
+
## Add to Claude Code
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
claude mcp add meltflex -- npx -y meltflex-mcp
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
…or add it to your MCP config (`.mcp.json` / client settings):
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"meltflex": {
|
|
30
|
+
"command": "npx",
|
|
31
|
+
"args": ["-y", "meltflex-mcp"],
|
|
32
|
+
"env": { "MELTFLEX_API_KEY": "mf_sk_xxxxxxxxxxxx" }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Cursor / Codex / other MCP clients use the same `command` + `args` + `env` shape.
|
|
39
|
+
|
|
40
|
+
## Tools
|
|
41
|
+
|
|
42
|
+
| Tool | What it does | Cost |
|
|
43
|
+
|------|--------------|------|
|
|
44
|
+
| `generate_interior` | Restyle a room photo into a photorealistic redesign. Inputs: `prompt`, `image` (path/URL/data-URL), optional `reference_images`, optional `output_path`. Saves the result to disk. | 10 credits |
|
|
45
|
+
| `check_credits` | Report the account's credit balance and per-operation costs. | free |
|
|
46
|
+
|
|
47
|
+
## Config / environment
|
|
48
|
+
|
|
49
|
+
| Variable | Default | Purpose |
|
|
50
|
+
|----------|---------|---------|
|
|
51
|
+
| `MELTFLEX_API_KEY` | — | API key (`mf_sk_...`). Overrides the config file. |
|
|
52
|
+
| `MELTFLEX_BASE_URL` | `https://www.meltflexai.com` | Override for staging/self-host. |
|
|
53
|
+
|
|
54
|
+
## Local development
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install
|
|
58
|
+
npm run build
|
|
59
|
+
node dist/cli.js whoami # sanity check the CLI
|
|
60
|
+
node dist/cli.js # run the MCP server on stdio
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Architecture
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
your agent ──stdio/MCP──► meltflex-mcp ──HTTPS Bearer mf_sk_──► api.meltflexai.com
|
|
67
|
+
/api/v1/generate
|
|
68
|
+
/api/v1/credits
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The MCP layer adds **no** new attack surface: anything it can do is already doable with a `curl` against the same API using the same key. Credit enforcement, refunds, and rate limits all live in the MeltFlex backend.
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { loadConfig } from './config.js';
|
|
3
|
+
const EXT_MIME = {
|
|
4
|
+
png: 'image/png',
|
|
5
|
+
jpg: 'image/jpeg',
|
|
6
|
+
jpeg: 'image/jpeg',
|
|
7
|
+
webp: 'image/webp',
|
|
8
|
+
gif: 'image/gif',
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Normalize any supported image input into a `data:image/...;base64,...` URL.
|
|
12
|
+
* Accepts: an existing data URL, an http(s) URL (fetched), or a local file path.
|
|
13
|
+
* Converting everything to a data URL guarantees every reference image is honored
|
|
14
|
+
* by the backend (which otherwise prefers URLs over base64 and won't mix them).
|
|
15
|
+
*/
|
|
16
|
+
async function resolveToDataUrl(input) {
|
|
17
|
+
if (/^data:image\//i.test(input))
|
|
18
|
+
return input;
|
|
19
|
+
if (/^https?:\/\//i.test(input)) {
|
|
20
|
+
const r = await fetch(input);
|
|
21
|
+
if (!r.ok)
|
|
22
|
+
throw new Error(`Failed to fetch image ${input} (HTTP ${r.status})`);
|
|
23
|
+
const buf = Buffer.from(await r.arrayBuffer());
|
|
24
|
+
const mime = r.headers.get('content-type')?.split(';')[0] || 'image/jpeg';
|
|
25
|
+
return `data:${mime};base64,${buf.toString('base64')}`;
|
|
26
|
+
}
|
|
27
|
+
// Treat as a local file path.
|
|
28
|
+
const ext = input.split('.').pop()?.toLowerCase() ?? 'png';
|
|
29
|
+
const mime = EXT_MIME[ext] ?? 'image/png';
|
|
30
|
+
const buf = readFileSync(input); // throws a clear ENOENT if the path is wrong
|
|
31
|
+
return `data:${mime};base64,${buf.toString('base64')}`;
|
|
32
|
+
}
|
|
33
|
+
export class MeltflexClient {
|
|
34
|
+
apiKey;
|
|
35
|
+
baseUrl;
|
|
36
|
+
constructor(apiKey, baseUrl) {
|
|
37
|
+
this.apiKey = apiKey;
|
|
38
|
+
this.baseUrl = baseUrl;
|
|
39
|
+
}
|
|
40
|
+
static fromConfig() {
|
|
41
|
+
const { apiKey, baseUrl } = loadConfig();
|
|
42
|
+
if (!apiKey) {
|
|
43
|
+
throw new Error('No MeltFlex API key configured. Run `meltflex auth <mf_sk_...>` or set the MELTFLEX_API_KEY environment variable. Get a key at https://www.meltflexai.com/settings (Growth plan or higher).');
|
|
44
|
+
}
|
|
45
|
+
return new MeltflexClient(apiKey, baseUrl);
|
|
46
|
+
}
|
|
47
|
+
headers() {
|
|
48
|
+
return {
|
|
49
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async generate(params) {
|
|
54
|
+
const body = { prompt: params.prompt };
|
|
55
|
+
body.image = await resolveToDataUrl(params.image);
|
|
56
|
+
if (params.referenceImages?.length) {
|
|
57
|
+
body.referenceImages = await Promise.all(params.referenceImages.map(resolveToDataUrl));
|
|
58
|
+
}
|
|
59
|
+
if (params.referenceProducts?.length) {
|
|
60
|
+
body.referenceProducts = params.referenceProducts;
|
|
61
|
+
}
|
|
62
|
+
const res = await fetch(`${this.baseUrl}/api/v1/generate`, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: this.headers(),
|
|
65
|
+
body: JSON.stringify(body),
|
|
66
|
+
});
|
|
67
|
+
const json = (await res.json().catch(() => ({})));
|
|
68
|
+
if (!res.ok || !json?.success) {
|
|
69
|
+
throw new Error(json?.message || json?.error || `Generation failed (HTTP ${res.status})`);
|
|
70
|
+
}
|
|
71
|
+
return { image: json.image, creditsUsed: json.creditsUsed };
|
|
72
|
+
}
|
|
73
|
+
async credits() {
|
|
74
|
+
const res = await fetch(`${this.baseUrl}/api/v1/credits`, {
|
|
75
|
+
headers: this.headers(),
|
|
76
|
+
});
|
|
77
|
+
const json = (await res.json().catch(() => ({})));
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
throw new Error(json?.error || `Credits check failed (HTTP ${res.status})`);
|
|
80
|
+
}
|
|
81
|
+
return json;
|
|
82
|
+
}
|
|
83
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { createServer } from './server.js';
|
|
4
|
+
import { MeltflexClient } from './api.js';
|
|
5
|
+
import { saveImage } from './util.js';
|
|
6
|
+
import { saveApiKey, loadConfig, CONFIG_PATH } from './config.js';
|
|
7
|
+
const USAGE = `MeltFlex AI — generate photorealistic interiors from your terminal or AI agent.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
meltflex Start the MCP server over stdio (used by AI agents)
|
|
11
|
+
meltflex auth <mf_sk_...> Save your MeltFlex API key
|
|
12
|
+
meltflex whoami Show authentication status
|
|
13
|
+
meltflex credits Show your credit balance and costs
|
|
14
|
+
meltflex generate [options] Generate a redesigned interior
|
|
15
|
+
|
|
16
|
+
generate options:
|
|
17
|
+
-i, --image <path|url> Source room photo (file path, http(s) URL, or data URL) [required]
|
|
18
|
+
-p, --prompt <text> Redesign instruction [required]
|
|
19
|
+
-r, --ref <path|url> Reference furniture image (repeatable, up to 10)
|
|
20
|
+
-o, --out <path> Where to save the result (default ./meltflex-output/)
|
|
21
|
+
|
|
22
|
+
Get an API key at https://www.meltflexai.com/settings (Growth plan or higher).
|
|
23
|
+
Config file: ${CONFIG_PATH}
|
|
24
|
+
`;
|
|
25
|
+
/** Minimal flag parser supporting repeatable --ref. */
|
|
26
|
+
function parseFlags(argv) {
|
|
27
|
+
const alias = {
|
|
28
|
+
'-i': 'image', '-p': 'prompt', '-r': 'ref', '-o': 'out',
|
|
29
|
+
'--image': 'image', '--prompt': 'prompt', '--ref': 'ref', '--out': 'out',
|
|
30
|
+
};
|
|
31
|
+
const out = {};
|
|
32
|
+
for (let i = 0; i < argv.length; i++) {
|
|
33
|
+
const key = alias[argv[i]];
|
|
34
|
+
if (!key)
|
|
35
|
+
continue;
|
|
36
|
+
const val = argv[++i];
|
|
37
|
+
if (val === undefined)
|
|
38
|
+
continue;
|
|
39
|
+
if (key === 'ref') {
|
|
40
|
+
(out.ref ??= []);
|
|
41
|
+
out.ref.push(val);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
out[key] = val;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
async function runServer() {
|
|
50
|
+
const server = createServer();
|
|
51
|
+
const transport = new StdioServerTransport();
|
|
52
|
+
await server.connect(transport);
|
|
53
|
+
// stdout is the JSON-RPC channel — only log to stderr.
|
|
54
|
+
console.error('[meltflex] MCP server running on stdio');
|
|
55
|
+
}
|
|
56
|
+
async function cmdGenerate(argv) {
|
|
57
|
+
const f = parseFlags(argv);
|
|
58
|
+
const image = f.image;
|
|
59
|
+
const prompt = f.prompt;
|
|
60
|
+
if (!image || !prompt) {
|
|
61
|
+
console.error('Error: --image and --prompt are required.\n\n' + USAGE);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const refs = f.ref ?? [];
|
|
65
|
+
const client = MeltflexClient.fromConfig();
|
|
66
|
+
console.error('Generating…');
|
|
67
|
+
const { image: dataUrl, creditsUsed } = await client.generate({
|
|
68
|
+
prompt,
|
|
69
|
+
image,
|
|
70
|
+
referenceImages: refs.length ? refs : undefined,
|
|
71
|
+
});
|
|
72
|
+
const target = saveImage(dataUrl, f.out);
|
|
73
|
+
console.log(`✅ Saved to ${target}`);
|
|
74
|
+
console.log(`Credits used: ${creditsUsed}`);
|
|
75
|
+
}
|
|
76
|
+
async function cmdCredits() {
|
|
77
|
+
const client = MeltflexClient.fromConfig();
|
|
78
|
+
const c = await client.credits();
|
|
79
|
+
console.log(`MeltFlex account${c.email ? ` (${c.email})` : ''}`);
|
|
80
|
+
console.log(`Balance: ${c.balance} credits (earned ${c.totalEarned}, spent ${c.totalSpent})`);
|
|
81
|
+
if (c.costs) {
|
|
82
|
+
console.log('Cost per operation:');
|
|
83
|
+
for (const [k, v] of Object.entries(c.costs))
|
|
84
|
+
console.log(` • ${k}: ${v}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function main() {
|
|
88
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
89
|
+
switch (cmd) {
|
|
90
|
+
case undefined:
|
|
91
|
+
case 'serve':
|
|
92
|
+
case 'mcp':
|
|
93
|
+
return runServer();
|
|
94
|
+
case 'auth': {
|
|
95
|
+
const key = rest[0]?.trim();
|
|
96
|
+
if (!key || !key.startsWith('mf_sk_')) {
|
|
97
|
+
console.error('Usage: meltflex auth <mf_sk_...>\nGet your API key at https://www.meltflexai.com/settings (Growth plan or higher).');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
console.log(`✅ API key saved to ${saveApiKey(key)}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
case 'whoami': {
|
|
104
|
+
const { apiKey, baseUrl } = loadConfig();
|
|
105
|
+
console.log(apiKey
|
|
106
|
+
? `Authenticated (key ${apiKey.slice(0, 12)}…) → ${baseUrl}`
|
|
107
|
+
: 'Not authenticated. Run: meltflex auth <mf_sk_...>');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
case 'credits':
|
|
111
|
+
return cmdCredits();
|
|
112
|
+
case 'generate':
|
|
113
|
+
return cmdGenerate(rest);
|
|
114
|
+
case 'help':
|
|
115
|
+
case '--help':
|
|
116
|
+
case '-h':
|
|
117
|
+
console.log(USAGE);
|
|
118
|
+
return;
|
|
119
|
+
default:
|
|
120
|
+
console.error(`Unknown command: ${cmd}\n\n${USAGE}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
main().catch((err) => {
|
|
125
|
+
console.error(`[meltflex] ${err?.message ?? err}`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
});
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, } from 'node:fs';
|
|
4
|
+
const CONFIG_DIR = join(homedir(), '.meltflex');
|
|
5
|
+
export const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
|
|
6
|
+
export const DEFAULT_BASE_URL = 'https://www.meltflexai.com';
|
|
7
|
+
function readFile() {
|
|
8
|
+
if (!existsSync(CONFIG_PATH))
|
|
9
|
+
return {};
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve config. Environment variables win over the on-disk config file so
|
|
19
|
+
* that hosted/CI setups (e.g. an `env` block in the MCP client config) work
|
|
20
|
+
* without an interactive `meltflex auth` step.
|
|
21
|
+
*/
|
|
22
|
+
export function loadConfig() {
|
|
23
|
+
const file = readFile();
|
|
24
|
+
return {
|
|
25
|
+
apiKey: process.env.MELTFLEX_API_KEY || file.apiKey,
|
|
26
|
+
baseUrl: process.env.MELTFLEX_BASE_URL || file.baseUrl || DEFAULT_BASE_URL,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Persist the API key to ~/.meltflex/config.json with owner-only perms. */
|
|
30
|
+
export function saveApiKey(apiKey) {
|
|
31
|
+
if (!existsSync(CONFIG_DIR))
|
|
32
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
33
|
+
const next = { ...readFile(), apiKey };
|
|
34
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(next, null, 2));
|
|
35
|
+
try {
|
|
36
|
+
chmodSync(CONFIG_PATH, 0o600);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* best-effort on platforms without POSIX perms */
|
|
40
|
+
}
|
|
41
|
+
return CONFIG_PATH;
|
|
42
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { MeltflexClient } from './api.js';
|
|
4
|
+
import { saveImage } from './util.js';
|
|
5
|
+
export function createServer() {
|
|
6
|
+
const server = new McpServer({ name: 'meltflex', version: '0.1.0' });
|
|
7
|
+
server.registerTool('generate_interior', {
|
|
8
|
+
title: 'Generate interior design',
|
|
9
|
+
description: 'Transform a room photo into a redesigned, photorealistic interior using MeltFlex AI. ' +
|
|
10
|
+
'Provide a source room photo (local file path, http(s) URL, or data:image URL) and a prompt ' +
|
|
11
|
+
'describing the desired style or changes. Optionally provide reference furniture/decor images ' +
|
|
12
|
+
'to place specific products into the room. Costs 10 MeltFlex credits per generation. ' +
|
|
13
|
+
'The result is saved to disk and the file path is returned.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
prompt: z
|
|
16
|
+
.string()
|
|
17
|
+
.describe('What to do to the room, e.g. "Redesign in warm Scandinavian style with light oak floors and a beige linen sofa". Room structure (walls, windows, doors) is preserved unless you say otherwise.'),
|
|
18
|
+
image: z
|
|
19
|
+
.string()
|
|
20
|
+
.describe('Source room photo: a local file path, an http(s) URL, or a data:image/...;base64,... URL.'),
|
|
21
|
+
reference_images: z
|
|
22
|
+
.array(z.string())
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('Optional furniture/decor reference images (file paths or URLs) whose exact appearance should be placed into the room.'),
|
|
25
|
+
output_path: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Where to save the result image. Defaults to ./meltflex-output/interior-<timestamp>.<ext> in the current working directory.'),
|
|
29
|
+
},
|
|
30
|
+
}, async ({ prompt, image, reference_images, output_path }) => {
|
|
31
|
+
try {
|
|
32
|
+
const client = MeltflexClient.fromConfig();
|
|
33
|
+
const { image: dataUrl, creditsUsed } = await client.generate({
|
|
34
|
+
prompt,
|
|
35
|
+
image,
|
|
36
|
+
referenceImages: reference_images,
|
|
37
|
+
});
|
|
38
|
+
const target = saveImage(dataUrl, output_path);
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: 'text',
|
|
43
|
+
text: `✅ Interior generated and saved to:\n${target}\n\nCredits used: ${creditsUsed}`,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
return {
|
|
50
|
+
isError: true,
|
|
51
|
+
content: [
|
|
52
|
+
{
|
|
53
|
+
type: 'text',
|
|
54
|
+
text: `MeltFlex generation failed: ${err?.message ?? String(err)}`,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
server.registerTool('check_credits', {
|
|
61
|
+
title: 'Check MeltFlex credits',
|
|
62
|
+
description: 'Check the current MeltFlex credit balance and the per-operation credit costs for the authenticated account.',
|
|
63
|
+
inputSchema: {},
|
|
64
|
+
}, async () => {
|
|
65
|
+
try {
|
|
66
|
+
const client = MeltflexClient.fromConfig();
|
|
67
|
+
const c = await client.credits();
|
|
68
|
+
const costLines = c.costs
|
|
69
|
+
? '\n\nCost per operation:\n' +
|
|
70
|
+
Object.entries(c.costs)
|
|
71
|
+
.map(([k, v]) => ` • ${k}: ${v} credits`)
|
|
72
|
+
.join('\n')
|
|
73
|
+
: '';
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: 'text',
|
|
78
|
+
text: `MeltFlex account${c.email ? ` (${c.email})` : ''}\n` +
|
|
79
|
+
`Balance: ${c.balance} credits\n` +
|
|
80
|
+
`Total earned: ${c.totalEarned} • Total spent: ${c.totalSpent}` +
|
|
81
|
+
costLines,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
return {
|
|
88
|
+
isError: true,
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: `Could not fetch credits: ${err?.message ?? String(err)}`,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return server;
|
|
99
|
+
}
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
3
|
+
/** Filesystem-safe timestamp like 2026-06-02_16-02-11. */
|
|
4
|
+
export function timestamp() {
|
|
5
|
+
return new Date()
|
|
6
|
+
.toISOString()
|
|
7
|
+
.replace(/[:.]/g, '-')
|
|
8
|
+
.replace('T', '_')
|
|
9
|
+
.slice(0, 19);
|
|
10
|
+
}
|
|
11
|
+
/** Decode a `data:image/...;base64,...` URL into a Buffer + a file extension. */
|
|
12
|
+
export function dataUrlToBuffer(dataUrl) {
|
|
13
|
+
const m = dataUrl.match(/^data:image\/(\w+);base64,(.+)$/);
|
|
14
|
+
if (!m)
|
|
15
|
+
throw new Error('Unexpected image format (expected a data:image/... URL)');
|
|
16
|
+
return {
|
|
17
|
+
buffer: Buffer.from(m[2], 'base64'),
|
|
18
|
+
ext: m[1] === 'jpeg' ? 'jpg' : m[1],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve where to write a generated image. If `outputPath` is given it is used
|
|
23
|
+
* (relative paths are resolved against cwd); otherwise a timestamped file under
|
|
24
|
+
* ./meltflex-output is returned.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveOutputPath(outputPath, ext) {
|
|
27
|
+
if (outputPath && outputPath.trim()) {
|
|
28
|
+
return isAbsolute(outputPath) ? outputPath : join(process.cwd(), outputPath);
|
|
29
|
+
}
|
|
30
|
+
return join(process.cwd(), 'meltflex-output', `interior-${timestamp()}.${ext}`);
|
|
31
|
+
}
|
|
32
|
+
/** Decode a result data URL and write it to disk, creating parent dirs. Returns the path. */
|
|
33
|
+
export function saveImage(dataUrl, outputPath) {
|
|
34
|
+
const { buffer, ext } = dataUrlToBuffer(dataUrl);
|
|
35
|
+
const target = resolveOutputPath(outputPath, ext);
|
|
36
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
37
|
+
writeFileSync(target, buffer);
|
|
38
|
+
return target;
|
|
39
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "meltflex-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MeltFlex AI — generate photorealistic interior designs from your AI agent. MCP server + CLI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"meltflex": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"skill",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"start": "node dist/cli.js",
|
|
20
|
+
"dev": "tsc && node dist/cli.js",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/MeltFlexDevs/MeltFlex_architecture.git",
|
|
29
|
+
"directory": "meltflex-app/meltflex-mcp"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://www.meltflexai.com/mcp",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"keywords": [
|
|
34
|
+
"mcp",
|
|
35
|
+
"model-context-protocol",
|
|
36
|
+
"meltflex",
|
|
37
|
+
"interior-design",
|
|
38
|
+
"ai",
|
|
39
|
+
"claude"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
43
|
+
"zod": "^3.23.8"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^20.11.0",
|
|
47
|
+
"typescript": "^5.4.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: meltflex
|
|
3
|
+
description: Generate photorealistic interior-design redesigns from a room photo using MeltFlex AI. Use when the user wants to restyle, redesign, or virtually stage a room, place specific furniture into a photo, or check their MeltFlex credit balance.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# MeltFlex AI
|
|
7
|
+
|
|
8
|
+
MeltFlex turns a real room photo into a redesigned, photorealistic interior. This skill drives the MeltFlex MCP server.
|
|
9
|
+
|
|
10
|
+
## Setup (once)
|
|
11
|
+
|
|
12
|
+
The MCP server must be connected and authenticated. The user authenticates to **their own** MeltFlex account; all generations are billed to their account credits.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx -y meltflex-mcp auth mf_sk_xxxxxxxx # or set MELTFLEX_API_KEY
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
If a tool reports "No MeltFlex API key configured", tell the user to run the command above. API keys come from https://www.meltflexai.com/settings (Growth plan or higher).
|
|
19
|
+
|
|
20
|
+
## Tools
|
|
21
|
+
|
|
22
|
+
### `generate_interior`
|
|
23
|
+
Transform a room photo. Cost: **10 credits** per generation.
|
|
24
|
+
- `prompt` (required) — the redesign instruction. Room structure (walls, windows, doors, flooring) is preserved unless the prompt says otherwise.
|
|
25
|
+
- `image` (required) — source room photo: local file path, http(s) URL, or `data:image` URL.
|
|
26
|
+
- `reference_images` (optional) — furniture/decor images to place exactly into the room.
|
|
27
|
+
- `output_path` (optional) — where to save the result. Defaults to `./meltflex-output/`.
|
|
28
|
+
|
|
29
|
+
### `check_credits`
|
|
30
|
+
Returns the account's credit balance and per-operation costs. Use this before a batch of generations, or when the user asks about credits.
|
|
31
|
+
|
|
32
|
+
## How to work with the user
|
|
33
|
+
|
|
34
|
+
1. **Always need a source photo.** If the user describes a room but gives no image, ask for a file path or URL — MeltFlex restyles an existing photo, it does not generate rooms from scratch.
|
|
35
|
+
2. **Write specific prompts.** Translate vague requests into concrete style language: materials, colors, furniture, lighting, mood. E.g. "make it cozy" → "Warm Scandinavian living room: light oak floor, beige linen sofa, cream wool rug, soft diffused daylight."
|
|
36
|
+
3. **Reference furniture** — when the user wants a *specific* product placed, pass its image(s) in `reference_images`.
|
|
37
|
+
4. **Report the saved path** and credits used after each generation. Offer iterations (the user can refine the prompt and regenerate).
|
|
38
|
+
5. **Low on credits?** If a generation fails with an insufficient-credits error, tell the user the balance and point them to https://www.meltflexai.com/settings.
|
|
39
|
+
|
|
40
|
+
## Example
|
|
41
|
+
|
|
42
|
+
> User: "Here's my living room /Users/me/room.jpg — make it modern minimalist."
|
|
43
|
+
|
|
44
|
+
Call `generate_interior` with:
|
|
45
|
+
- `prompt`: "Modern minimalist living room: white walls, polished concrete floor, low-profile grey sofa, single statement floor lamp, decluttered, natural light. Preserve windows and doors."
|
|
46
|
+
- `image`: "/Users/me/room.jpg"
|